This commit was manufactured by cvs2svn to create tag 'freeside_1_5_0pre3'. freeside_1_5_0pre3
authorcvs2git <cvs2git>
Tue, 15 Jul 2003 11:23:22 +0000 (11:23 +0000)
committercvs2git <cvs2git>
Tue, 15 Jul 2003 11:23:22 +0000 (11:23 +0000)
939 files changed:
Artistic [deleted file]
CREDITS
FS/Changes [new file with mode: 0644]
FS/FS.pm [new file with mode: 0644]
FS/FS/CGI.pm [new file with mode: 0644]
FS/FS/ClientAPI.pm [new file with mode: 0644]
FS/FS/ClientAPI/MyAccount.pm [new file with mode: 0644]
FS/FS/ClientAPI/passwd.pm [new file with mode: 0644]
FS/FS/Conf.pm [new file with mode: 0644]
FS/FS/ConfItem.pm [new file with mode: 0644]
FS/FS/InitHandler.pm [new file with mode: 0644]
FS/FS/Misc.pm [new file with mode: 0644]
FS/FS/Msgcat.pm [new file with mode: 0644]
FS/FS/Record.pm [new file with mode: 0644]
FS/FS/SearchCache.pm [new file with mode: 0644]
FS/FS/UI/Base.pm [new file with mode: 0644]
FS/FS/UI/CGI.pm [new file with mode: 0644]
FS/FS/UI/Gtk.pm [new file with mode: 0644]
FS/FS/UI/agent.pm [new file with mode: 0644]
FS/FS/UID.pm [new file with mode: 0644]
FS/FS/addr_block.pm [new file with mode: 0755]
FS/FS/agent.pm [new file with mode: 0644]
FS/FS/agent_type.pm [new file with mode: 0644]
FS/FS/cust_bill.pm [new file with mode: 0644]
FS/FS/cust_bill_event.pm [new file with mode: 0644]
FS/FS/cust_bill_pay.pm [new file with mode: 0644]
FS/FS/cust_bill_pkg.pm [new file with mode: 0644]
FS/FS/cust_bill_pkg_detail.pm [new file with mode: 0644]
FS/FS/cust_credit.pm [new file with mode: 0644]
FS/FS/cust_credit_bill.pm [new file with mode: 0644]
FS/FS/cust_credit_refund.pm [new file with mode: 0644]
FS/FS/cust_main.pm [new file with mode: 0644]
FS/FS/cust_main_county.pm [new file with mode: 0644]
FS/FS/cust_main_invoice.pm [new file with mode: 0644]
FS/FS/cust_pay.pm [new file with mode: 0644]
FS/FS/cust_pay_batch.pm [new file with mode: 0644]
FS/FS/cust_pkg.pm [new file with mode: 0644]
FS/FS/cust_refund.pm [new file with mode: 0644]
FS/FS/cust_svc.pm [new file with mode: 0644]
FS/FS/cust_tax_exempt.pm [new file with mode: 0644]
FS/FS/domain_record.pm [new file with mode: 0644]
FS/FS/export_svc.pm [new file with mode: 0644]
FS/FS/msgcat.pm [new file with mode: 0644]
FS/FS/nas.pm [new file with mode: 0644]
FS/FS/part_bill_event.pm [new file with mode: 0644]
FS/FS/part_export.pm [new file with mode: 0644]
FS/FS/part_export/apache.pm [new file with mode: 0644]
FS/FS/part_export/bind.pm [new file with mode: 0644]
FS/FS/part_export/bind_slave.pm [new file with mode: 0644]
FS/FS/part_export/bsdshell.pm [new file with mode: 0644]
FS/FS/part_export/cp.pm [new file with mode: 0644]
FS/FS/part_export/cyrus.pm [new file with mode: 0644]
FS/FS/part_export/domain_shellcommands.pm [new file with mode: 0644]
FS/FS/part_export/forward_shellcommands.pm [new file with mode: 0644]
FS/FS/part_export/http.pm [new file with mode: 0644]
FS/FS/part_export/infostreet.pm [new file with mode: 0644]
FS/FS/part_export/ldap.pm [new file with mode: 0644]
FS/FS/part_export/null.pm [new file with mode: 0644]
FS/FS/part_export/shellcommands.pm [new file with mode: 0644]
FS/FS/part_export/shellcommands_withdomain.pm [new file with mode: 0644]
FS/FS/part_export/sqlmail.pm [new file with mode: 0644]
FS/FS/part_export/sqlradius.pm [new file with mode: 0644]
FS/FS/part_export/sqlradius_withdomain.pm [new file with mode: 0644]
FS/FS/part_export/sysvshell.pm [new file with mode: 0644]
FS/FS/part_export/textradius.pm [new file with mode: 0644]
FS/FS/part_export/vpopmail.pm [new file with mode: 0644]
FS/FS/part_export/www_shellcommands.pm [new file with mode: 0644]
FS/FS/part_export_option.pm [new file with mode: 0644]
FS/FS/part_pkg.pm [new file with mode: 0644]
FS/FS/part_pop_local.pm [new file with mode: 0644]
FS/FS/part_referral.pm [new file with mode: 0644]
FS/FS/part_router_field.pm [new file with mode: 0755]
FS/FS/part_sb_field.pm [new file with mode: 0755]
FS/FS/part_svc.pm [new file with mode: 0644]
FS/FS/part_svc_column.pm [new file with mode: 0644]
FS/FS/part_svc_router.pm [new file with mode: 0755]
FS/FS/pkg_svc.pm [new file with mode: 0644]
FS/FS/port.pm [new file with mode: 0644]
FS/FS/prepay_credit.pm [new file with mode: 0644]
FS/FS/queue.pm [new file with mode: 0644]
FS/FS/queue_arg.pm [new file with mode: 0644]
FS/FS/queue_depend.pm [new file with mode: 0644]
FS/FS/raddb.pm [new file with mode: 0644]
FS/FS/radius_usergroup.pm [new file with mode: 0644]
FS/FS/router.pm [new file with mode: 0755]
FS/FS/router_field.pm [new file with mode: 0755]
FS/FS/sb_field.pm [new file with mode: 0755]
FS/FS/session.pm [new file with mode: 0644]
FS/FS/svc_Common.pm [new file with mode: 0644]
FS/FS/svc_acct.pm [new file with mode: 0644]
FS/FS/svc_acct_pop.pm [new file with mode: 0644]
FS/FS/svc_broadband.pm [new file with mode: 0755]
FS/FS/svc_domain.pm [new file with mode: 0644]
FS/FS/svc_forward.pm [new file with mode: 0644]
FS/FS/svc_www.pm [new file with mode: 0644]
FS/FS/type_pkgs.pm [new file with mode: 0644]
FS/MANIFEST [new file with mode: 0644]
FS/MANIFEST.SKIP [new file with mode: 0644]
FS/Makefile.PL [new file with mode: 0644]
FS/README [new file with mode: 0644]
FS/bin/freeside-addoutsource [new file with mode: 0644]
FS/bin/freeside-addoutsourceuser [new file with mode: 0644]
FS/bin/freeside-adduser [new file with mode: 0644]
FS/bin/freeside-apply-credits [new file with mode: 0755]
FS/bin/freeside-bill [new file with mode: 0755]
FS/bin/freeside-cc-receipts-report [new file with mode: 0755]
FS/bin/freeside-count-active-customers [new file with mode: 0755]
FS/bin/freeside-credit-report [new file with mode: 0755]
FS/bin/freeside-daily [new file with mode: 0755]
FS/bin/freeside-deloutsource [new file with mode: 0644]
FS/bin/freeside-deloutsourceuser [new file with mode: 0644]
FS/bin/freeside-deluser [new file with mode: 0644]
FS/bin/freeside-email [new file with mode: 0755]
FS/bin/freeside-expiration-alerter [new file with mode: 0755]
FS/bin/freeside-queued [new file with mode: 0644]
FS/bin/freeside-radgroup [new file with mode: 0644]
FS/bin/freeside-receivables-report [new file with mode: 0755]
FS/bin/freeside-reexport [new file with mode: 0644]
FS/bin/freeside-selfservice-server [new file with mode: 0644]
FS/bin/freeside-setinvoice [new file with mode: 0644]
FS/bin/freeside-setup [new file with mode: 0755]
FS/bin/freeside-sqlradius-radacctd [new file with mode: 0644]
FS/bin/freeside-sqlradius-reset [new file with mode: 0755]
FS/bin/freeside-sqlradius-seconds [new file with mode: 0644]
FS/bin/freeside-tax-report [new file with mode: 0755]
FS/t/CGI.t [new file with mode: 0644]
FS/t/ClientAPI.t [new file with mode: 0644]
FS/t/Conf.t [new file with mode: 0644]
FS/t/ConfItem.t [new file with mode: 0644]
FS/t/InitHandler.t [new file with mode: 0644]
FS/t/Misc.t [new file with mode: 0644]
FS/t/Msgcat.t [new file with mode: 0644]
FS/t/Record.t [new file with mode: 0644]
FS/t/SearchCache.t [new file with mode: 0644]
FS/t/UID.t [new file with mode: 0644]
FS/t/agent.t [new file with mode: 0644]
FS/t/agent_type.t [new file with mode: 0644]
FS/t/cust_bill.t [new file with mode: 0644]
FS/t/cust_bill_event.t [new file with mode: 0644]
FS/t/cust_bill_pay.t [new file with mode: 0644]
FS/t/cust_bill_pkg.t [new file with mode: 0644]
FS/t/cust_bill_pkg_detail.t [new file with mode: 0644]
FS/t/cust_credit.t [new file with mode: 0644]
FS/t/cust_credit_bill.t [new file with mode: 0644]
FS/t/cust_credit_refund.t [new file with mode: 0644]
FS/t/cust_main.t [new file with mode: 0644]
FS/t/cust_main_county.t [new file with mode: 0644]
FS/t/cust_main_invoice.t [new file with mode: 0644]
FS/t/cust_pay.t [new file with mode: 0644]
FS/t/cust_pay_batch.t [new file with mode: 0644]
FS/t/cust_pkg.t [new file with mode: 0644]
FS/t/cust_refund.t [new file with mode: 0644]
FS/t/cust_svc.t [new file with mode: 0644]
FS/t/cust_tax_exempt.pm [new file with mode: 0644]
FS/t/cust_tax_exempt.t [new file with mode: 0644]
FS/t/domain_record.t [new file with mode: 0644]
FS/t/export_svc.t [new file with mode: 0644]
FS/t/msgcat.t [new file with mode: 0644]
FS/t/nas.t [new file with mode: 0644]
FS/t/part_bill_event.t [new file with mode: 0644]
FS/t/part_export-apache.t [new file with mode: 0644]
FS/t/part_export-bind.t [new file with mode: 0644]
FS/t/part_export-bind_slave.t [new file with mode: 0644]
FS/t/part_export-bsdshell.t [new file with mode: 0644]
FS/t/part_export-cp.t [new file with mode: 0644]
FS/t/part_export-cyrus.t [new file with mode: 0644]
FS/t/part_export-domain_shellcommands.t [new file with mode: 0644]
FS/t/part_export-forward_shellcommands.t [new file with mode: 0644]
FS/t/part_export-http.t [new file with mode: 0644]
FS/t/part_export-infostreet.t [new file with mode: 0644]
FS/t/part_export-ldap.t [new file with mode: 0644]
FS/t/part_export-null.t [new file with mode: 0644]
FS/t/part_export-shellcommands.t [new file with mode: 0644]
FS/t/part_export-shellcommands_withdomain.t [new file with mode: 0644]
FS/t/part_export-sqlmail.t [new file with mode: 0644]
FS/t/part_export-sqlradius.t [new file with mode: 0644]
FS/t/part_export-sqlradius_withdomain.t [new file with mode: 0644]
FS/t/part_export-sysvshell.t [new file with mode: 0644]
FS/t/part_export-textradius.t [new file with mode: 0644]
FS/t/part_export-vpopmail.t [new file with mode: 0644]
FS/t/part_export-www_shellcommands.t [new file with mode: 0644]
FS/t/part_export.t [new file with mode: 0644]
FS/t/part_export_option.t [new file with mode: 0644]
FS/t/part_pkg.t [new file with mode: 0644]
FS/t/part_pop_local.t [new file with mode: 0644]
FS/t/part_referral.t [new file with mode: 0644]
FS/t/part_svc.t [new file with mode: 0644]
FS/t/part_svc_column.t [new file with mode: 0644]
FS/t/pkg_svc.t [new file with mode: 0644]
FS/t/port.t [new file with mode: 0644]
FS/t/prepay_credit.t [new file with mode: 0644]
FS/t/queue.t [new file with mode: 0644]
FS/t/queue_arg.t [new file with mode: 0644]
FS/t/queue_depend.t [new file with mode: 0644]
FS/t/raddb.t [new file with mode: 0644]
FS/t/radius_usergroup.t [new file with mode: 0644]
FS/t/session.t [new file with mode: 0644]
FS/t/svc_Common.t [new file with mode: 0644]
FS/t/svc_acct.t [new file with mode: 0644]
FS/t/svc_acct_pop.t [new file with mode: 0644]
FS/t/svc_domain.t [new file with mode: 0644]
FS/t/svc_forward.t [new file with mode: 0644]
FS/t/svc_www.t [new file with mode: 0644]
FS/t/type_pkgs.t [new file with mode: 0644]
INSTALL
Makefile [new file with mode: 0644]
README
README.1.5.0pre1 [new file with mode: 0644]
TODO
bin/apache.export [new file with mode: 0755]
bin/bill [deleted file]
bin/bind.export [new file with mode: 0755]
bin/bind.import [new file with mode: 0755]
bin/bsdshell.export [new file with mode: 0755]
bin/create-history-tables [new file with mode: 0755]
bin/dbdef-create
bin/fix-sequences [new file with mode: 0755]
bin/freeside-init [new file with mode: 0755]
bin/freeside-session-kill [new file with mode: 0755]
bin/fs-migrate-part_svc [new file with mode: 0755]
bin/fs-migrate-payref [new file with mode: 0755]
bin/fs-migrate-svc_acct_sm [new file with mode: 0755]
bin/fs-radius-add-check [new file with mode: 0755]
bin/fs-radius-add-reply [new file with mode: 0755]
bin/fs-setup [deleted file]
bin/generate-prepay [new file with mode: 0755]
bin/generate-raddb [new file with mode: 0755]
bin/generate-tests [new file with mode: 0755]
bin/masonize [new file with mode: 0755]
bin/passwd.import [new file with mode: 0755]
bin/pod2x
bin/populate-msgcat [new file with mode: 0755]
bin/svc_acct.export
bin/svc_acct.import
bin/svc_acct_sm.export [deleted file]
bin/svc_acct_sm.import [deleted file]
bin/svc_domain.erase [new file with mode: 0755]
bin/sysvshell.export [new file with mode: 0755]
conf/address [deleted file]
conf/agent_defaultpkg [new file with mode: 0644]
conf/alerter_template [new file with mode: 0644]
conf/declinetemplate [new file with mode: 0644]
conf/domain [deleted file]
conf/invoice_from [new file with mode: 0644]
conf/invoice_template [new file with mode: 0644]
conf/locale [new file with mode: 0644]
conf/maxsearchrecordsperpage [new file with mode: 0644]
conf/registries/internic/from [deleted file]
conf/registries/internic/nameservers [deleted file]
conf/registries/internic/tech_contact [deleted file]
conf/registries/internic/template [deleted file]
conf/registries/internic/to [deleted file]
conf/report_template [new file with mode: 0644]
conf/secrets [deleted file]
conf/shells
conf/show-msgcat-codes [new file with mode: 0644]
conf/smtpmachine
conf/soadefaultttl [new file with mode: 0644]
conf/soaexpire [new file with mode: 0644]
conf/soarefresh [new file with mode: 0644]
conf/soaretry [new file with mode: 0644]
debian/README.Debian [new file with mode: 0644]
debian/changelog [new file with mode: 0644]
debian/conffiles.ex [new file with mode: 0644]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/cron.d.ex [new file with mode: 0644]
debian/dirs [new file with mode: 0644]
debian/docs [new file with mode: 0644]
debian/ex.doc-base.package [new file with mode: 0644]
debian/freeside-doc.docs [new file with mode: 0644]
debian/freeside-doc.files [new file with mode: 0644]
debian/init.d.ex [new file with mode: 0644]
debian/manpage.1.ex [new file with mode: 0644]
debian/manpage.sgml.ex [new file with mode: 0644]
debian/menu.ex [new file with mode: 0644]
debian/postinst.ex [new file with mode: 0644]
debian/postrm.ex [new file with mode: 0644]
debian/preinst.ex [new file with mode: 0644]
debian/prerm.ex [new file with mode: 0644]
debian/rules [new file with mode: 0755]
debian/watch.ex [new file with mode: 0644]
eg/TEMPLATE_cust_main.import
eg/export_template.pm [new file with mode: 0644]
eg/table_template-svc.pm [new file with mode: 0644]
eg/table_template.pm [new file with mode: 0644]
etc/abbr_state.txt [new file with mode: 0644]
etc/acp_logfile-parse [deleted file]
etc/example-direct-cardin [deleted file]
etc/megapop.pl [new file with mode: 0755]
etc/sql-reserved-words.txt [new file with mode: 0644]
fs_passwd/fs_passwd
fs_passwd/fs_passwd.cgi [new file with mode: 0755]
fs_passwd/fs_passwd.html [new file with mode: 0644]
fs_passwd/fs_passwd_server
fs_passwd/fs_passwdd
fs_selfadmin/FS-MailAdminServer/MailAdminClient.pm [new file with mode: 0755]
fs_selfadmin/FS-MailAdminServer/cgi/mailadmin.cgi [new file with mode: 0755]
fs_selfadmin/FS-MailAdminServer/fs_mailadmind [new file with mode: 0755]
fs_selfadmin/README [new file with mode: 0644]
fs_selfadmin/fs_mailadmin_server [new file with mode: 0755]
fs_selfservice/DEPLOY [new file with mode: 0755]
fs_selfservice/FS-SelfService/Changes [new file with mode: 0644]
fs_selfservice/FS-SelfService/MANIFEST [new file with mode: 0644]
fs_selfservice/FS-SelfService/Makefile.PL [new file with mode: 0644]
fs_selfservice/FS-SelfService/SelfService.pm [new file with mode: 0644]
fs_selfservice/FS-SelfService/cgi/login.html [new file with mode: 0644]
fs_selfservice/FS-SelfService/cgi/make_payment.html [new file with mode: 0644]
fs_selfservice/FS-SelfService/cgi/myaccount.html [new file with mode: 0644]
fs_selfservice/FS-SelfService/cgi/passwd.html [new file with mode: 0644]
fs_selfservice/FS-SelfService/cgi/payment_results.html [new file with mode: 0644]
fs_selfservice/FS-SelfService/cgi/selfservice.cgi [new file with mode: 0644]
fs_selfservice/FS-SelfService/cgi/view_invoice.html [new file with mode: 0644]
fs_selfservice/FS-SelfService/freeside-selfservice-clientd [new file with mode: 0644]
fs_selfservice/FS-SelfService/test.pl [new file with mode: 0644]
fs_selfservice/fs_passwd_test [new file with mode: 0755]
fs_sesmon/FS-SessionClient/Changes [new file with mode: 0644]
fs_sesmon/FS-SessionClient/MANIFEST [new file with mode: 0644]
fs_sesmon/FS-SessionClient/MANIFEST.SKIP [new file with mode: 0644]
fs_sesmon/FS-SessionClient/Makefile.PL [new file with mode: 0644]
fs_sesmon/FS-SessionClient/SessionClient.pm [new file with mode: 0644]
fs_sesmon/FS-SessionClient/bin/freeside-login [new file with mode: 0644]
fs_sesmon/FS-SessionClient/bin/freeside-logout [new file with mode: 0644]
fs_sesmon/FS-SessionClient/cgi/login.cgi [new file with mode: 0644]
fs_sesmon/FS-SessionClient/cgi/logout.cgi [new file with mode: 0644]
fs_sesmon/FS-SessionClient/fs_sessiond [new file with mode: 0644]
fs_sesmon/FS-SessionClient/test.pl [new file with mode: 0644]
fs_sesmon/fs_session_server [new file with mode: 0644]
fs_signup/FS-SignupClient/Changes [new file with mode: 0644]
fs_signup/FS-SignupClient/MANIFEST [new file with mode: 0644]
fs_signup/FS-SignupClient/MANIFEST.SKIP [new file with mode: 0644]
fs_signup/FS-SignupClient/Makefile.PL [new file with mode: 0644]
fs_signup/FS-SignupClient/SignupClient.pm [new file with mode: 0644]
fs_signup/FS-SignupClient/cgi/decline.html [new file with mode: 0644]
fs_signup/FS-SignupClient/cgi/signup-alternate.html [new file with mode: 0755]
fs_signup/FS-SignupClient/cgi/signup.cgi [new file with mode: 0755]
fs_signup/FS-SignupClient/cgi/signup.html [new file with mode: 0755]
fs_signup/FS-SignupClient/cgi/stateselect.html [new file with mode: 0644]
fs_signup/FS-SignupClient/cgi/success.html [new file with mode: 0644]
fs_signup/FS-SignupClient/fs_signupd [new file with mode: 0755]
fs_signup/FS-SignupClient/test.pl [new file with mode: 0644]
fs_signup/cck.template [new file with mode: 0644]
fs_signup/fs_signup_server [new file with mode: 0755]
fs_signup/ieak.template [new file with mode: 0755]
fs_webdemo/register.cgi [new file with mode: 0755]
fs_webdemo/register.html [new file with mode: 0644]
fs_webdemo/registerd [new file with mode: 0755]
fs_webdemo/registerd.Pg [new file with mode: 0755]
htdocs/browse/agent.cgi [deleted file]
htdocs/browse/agent_type.cgi [deleted file]
htdocs/browse/cust_main_county.cgi [deleted file]
htdocs/browse/part_pkg.cgi [deleted file]
htdocs/browse/part_referral.cgi [deleted file]
htdocs/browse/part_svc.cgi [deleted file]
htdocs/browse/svc_acct_pop.cgi [deleted file]
htdocs/docs/CGI-modules-2.76-patch.txt [deleted file]
htdocs/docs/admin.html [deleted file]
htdocs/docs/billing.html [deleted file]
htdocs/docs/config.html [deleted file]
htdocs/docs/export.html [deleted file]
htdocs/docs/index.html [deleted file]
htdocs/docs/install.html [deleted file]
htdocs/docs/legacy.html [deleted file]
htdocs/docs/man/Bill.txt [deleted file]
htdocs/docs/man/CGI.txt [deleted file]
htdocs/docs/man/Conf.txt [deleted file]
htdocs/docs/man/Invoice.txt [deleted file]
htdocs/docs/man/Record.txt [deleted file]
htdocs/docs/man/SSH.txt [deleted file]
htdocs/docs/man/UID.txt [deleted file]
htdocs/docs/man/agent.txt [deleted file]
htdocs/docs/man/agent_type.txt [deleted file]
htdocs/docs/man/cust_bill.txt [deleted file]
htdocs/docs/man/cust_bill_pkg.txt [deleted file]
htdocs/docs/man/cust_credit.txt [deleted file]
htdocs/docs/man/cust_main.txt [deleted file]
htdocs/docs/man/cust_main_county.txt [deleted file]
htdocs/docs/man/cust_pay.txt [deleted file]
htdocs/docs/man/cust_pkg.txt [deleted file]
htdocs/docs/man/cust_refund.txt [deleted file]
htdocs/docs/man/cust_svc.txt [deleted file]
htdocs/docs/man/dbdef.txt [deleted file]
htdocs/docs/man/dbdef_colgroup.txt [deleted file]
htdocs/docs/man/dbdef_column.txt [deleted file]
htdocs/docs/man/dbdef_index.txt [deleted file]
htdocs/docs/man/dbdef_table.txt [deleted file]
htdocs/docs/man/dbdef_unique.txt [deleted file]
htdocs/docs/man/index.html [deleted file]
htdocs/docs/man/part_pkg.txt [deleted file]
htdocs/docs/man/part_referral.txt [deleted file]
htdocs/docs/man/part_svc.txt [deleted file]
htdocs/docs/man/pkg_svc.txt [deleted file]
htdocs/docs/man/svc_acct.txt [deleted file]
htdocs/docs/man/svc_acct_pop.txt [deleted file]
htdocs/docs/man/svc_acct_sm.txt [deleted file]
htdocs/docs/man/svc_domain.txt [deleted file]
htdocs/docs/man/type_pkgs.txt [deleted file]
htdocs/docs/passwd.html [deleted file]
htdocs/docs/schema.html [deleted file]
htdocs/docs/trouble.html [deleted file]
htdocs/docs/upgrade.html [deleted file]
htdocs/docs/upgrade2.html [deleted file]
htdocs/edit/agent.cgi [deleted file]
htdocs/edit/agent_type.cgi [deleted file]
htdocs/edit/cust_credit.cgi [deleted file]
htdocs/edit/cust_main.cgi [deleted file]
htdocs/edit/cust_main_county-expand.cgi [deleted file]
htdocs/edit/cust_main_county.cgi [deleted file]
htdocs/edit/cust_pay.cgi [deleted file]
htdocs/edit/cust_pkg.cgi [deleted file]
htdocs/edit/part_pkg.cgi [deleted file]
htdocs/edit/part_referral.cgi [deleted file]
htdocs/edit/part_svc.cgi [deleted file]
htdocs/edit/process/agent.cgi [deleted file]
htdocs/edit/process/agent_type.cgi [deleted file]
htdocs/edit/process/cust_credit.cgi [deleted file]
htdocs/edit/process/cust_main.cgi [deleted file]
htdocs/edit/process/cust_main_county-expand.cgi [deleted file]
htdocs/edit/process/cust_main_county.cgi [deleted file]
htdocs/edit/process/cust_pay.cgi [deleted file]
htdocs/edit/process/cust_pkg.cgi [deleted file]
htdocs/edit/process/part_pkg.cgi [deleted file]
htdocs/edit/process/part_referral.cgi [deleted file]
htdocs/edit/process/part_svc.cgi [deleted file]
htdocs/edit/process/svc_acct.cgi [deleted file]
htdocs/edit/process/svc_acct_pop.cgi [deleted file]
htdocs/edit/process/svc_acct_sm.cgi [deleted file]
htdocs/edit/process/svc_domain.cgi [deleted file]
htdocs/edit/svc_acct.cgi [deleted file]
htdocs/edit/svc_acct_pop.cgi [deleted file]
htdocs/edit/svc_acct_sm.cgi [deleted file]
htdocs/edit/svc_domain.cgi [deleted file]
htdocs/images/mid-logo.gif [deleted file]
htdocs/images/sisd.jpg [deleted file]
htdocs/images/small-logo.gif [deleted file]
htdocs/index.html [deleted file]
htdocs/misc/bill.cgi [deleted file]
htdocs/misc/cancel-unaudited.cgi [deleted file]
htdocs/misc/cancel_pkg.cgi [deleted file]
htdocs/misc/expire_pkg.cgi [deleted file]
htdocs/misc/link.cgi [deleted file]
htdocs/misc/print-invoice.cgi [deleted file]
htdocs/misc/process/link.cgi [deleted file]
htdocs/misc/susp_pkg.cgi [deleted file]
htdocs/misc/unsusp_pkg.cgi [deleted file]
htdocs/search/cust_bill.cgi [deleted file]
htdocs/search/cust_bill.html [deleted file]
htdocs/search/cust_main-payinfo.html [deleted file]
htdocs/search/cust_main.cgi [deleted file]
htdocs/search/cust_main.html [deleted file]
htdocs/search/cust_pkg.cgi [deleted file]
htdocs/search/svc_acct.cgi [deleted file]
htdocs/search/svc_acct.html [deleted file]
htdocs/search/svc_acct_sm.cgi [deleted file]
htdocs/search/svc_acct_sm.html [deleted file]
htdocs/search/svc_domain.cgi [deleted file]
htdocs/search/svc_domain.html [deleted file]
htdocs/view/cust_bill.cgi [deleted file]
htdocs/view/cust_main.cgi [deleted file]
htdocs/view/cust_pkg.cgi [deleted file]
htdocs/view/svc_acct.cgi [deleted file]
htdocs/view/svc_acct_sm.cgi [deleted file]
htdocs/view/svc_domain.cgi [deleted file]
htetc/global.asa [new file with mode: 0644]
htetc/handler.pl [new file with mode: 0644]
htetc/handler.pl-1.0x [new file with mode: 0644]
httemplate/.htaccess [new file with mode: 0755]
httemplate/browse/addr_block.cgi [new file with mode: 0644]
httemplate/browse/agent.cgi [new file with mode: 0755]
httemplate/browse/agent_type.cgi [new file with mode: 0755]
httemplate/browse/cust_main_county.cgi [new file with mode: 0755]
httemplate/browse/cust_pay_batch.cgi [new file with mode: 0755]
httemplate/browse/generic.cgi [new file with mode: 0644]
httemplate/browse/msgcat.cgi [new file with mode: 0755]
httemplate/browse/nas.cgi [new file with mode: 0755]
httemplate/browse/part_bill_event.cgi [new file with mode: 0755]
httemplate/browse/part_export.cgi [new file with mode: 0755]
httemplate/browse/part_pkg.cgi [new file with mode: 0755]
httemplate/browse/part_referral.cgi [new file with mode: 0755]
httemplate/browse/part_sb_field.cgi [new file with mode: 0644]
httemplate/browse/part_svc.cgi [new file with mode: 0755]
httemplate/browse/queue.cgi [new file with mode: 0755]
httemplate/browse/router.cgi [new file with mode: 0644]
httemplate/browse/svc_acct_pop.cgi [new file with mode: 0755]
httemplate/config/config-process.cgi [new file with mode: 0644]
httemplate/config/config-view.cgi [new file with mode: 0644]
httemplate/config/config.cgi [new file with mode: 0644]
httemplate/docs/admin.html [new file with mode: 0755]
httemplate/docs/billing.html [new file with mode: 0644]
httemplate/docs/config.html [new file with mode: 0644]
httemplate/docs/export.html [new file with mode: 0755]
httemplate/docs/index.html [new file with mode: 0644]
httemplate/docs/install.html [new file with mode: 0644]
httemplate/docs/legacy.html [new file with mode: 0755]
httemplate/docs/man/FS/part_export/.cvs_is_on_crack [new file with mode: 0644]
httemplate/docs/overview.dia [new file with mode: 0644]
httemplate/docs/overview.png [new file with mode: 0644]
httemplate/docs/passwd.html [new file with mode: 0755]
httemplate/docs/schema.dia [new file with mode: 0644]
httemplate/docs/schema.html [new file with mode: 0644]
httemplate/docs/schema.png [new file with mode: 0644]
httemplate/docs/session.html [new file with mode: 0644]
httemplate/docs/signup.html [new file with mode: 0644]
httemplate/docs/ssh.html [new file with mode: 0755]
httemplate/docs/trouble.html [new file with mode: 0755]
httemplate/docs/upgrade10.html [new file with mode: 0644]
httemplate/docs/upgrade7.html [new file with mode: 0644]
httemplate/docs/upgrade8.html [new file with mode: 0644]
httemplate/docs/upgrade9.html [new file with mode: 0644]
httemplate/edit/REAL_cust_pkg.cgi [new file with mode: 0755]
httemplate/edit/agent.cgi [new file with mode: 0755]
httemplate/edit/agent_type.cgi [new file with mode: 0755]
httemplate/edit/cust_bill_pay.cgi [new file with mode: 0755]
httemplate/edit/cust_credit.cgi [new file with mode: 0755]
httemplate/edit/cust_credit_bill.cgi [new file with mode: 0755]
httemplate/edit/cust_main.cgi [new file with mode: 0755]
httemplate/edit/cust_main_county-expand.cgi [new file with mode: 0755]
httemplate/edit/cust_main_county.cgi [new file with mode: 0755]
httemplate/edit/cust_pay.cgi [new file with mode: 0755]
httemplate/edit/cust_pkg.cgi [new file with mode: 0755]
httemplate/edit/msgcat.cgi [new file with mode: 0755]
httemplate/edit/part_bill_event.cgi [new file with mode: 0755]
httemplate/edit/part_export.cgi [new file with mode: 0644]
httemplate/edit/part_pkg.cgi [new file with mode: 0755]
httemplate/edit/part_referral.cgi [new file with mode: 0755]
httemplate/edit/part_router_field.cgi [new file with mode: 0644]
httemplate/edit/part_sb_field.cgi [new file with mode: 0644]
httemplate/edit/part_svc.cgi [new file with mode: 0755]
httemplate/edit/process/REAL_cust_pkg.cgi [new file with mode: 0755]
httemplate/edit/process/addr_block/add.cgi [new file with mode: 0755]
httemplate/edit/process/addr_block/allocate.cgi [new file with mode: 0755]
httemplate/edit/process/addr_block/deallocate.cgi [new file with mode: 0755]
httemplate/edit/process/addr_block/split.cgi [new file with mode: 0755]
httemplate/edit/process/agent.cgi [new file with mode: 0755]
httemplate/edit/process/agent_type.cgi [new file with mode: 0755]
httemplate/edit/process/cust_bill_pay.cgi [new file with mode: 0755]
httemplate/edit/process/cust_credit.cgi [new file with mode: 0755]
httemplate/edit/process/cust_credit_bill.cgi [new file with mode: 0755]
httemplate/edit/process/cust_main.cgi [new file with mode: 0755]
httemplate/edit/process/cust_main_county-collapse.cgi [new file with mode: 0755]
httemplate/edit/process/cust_main_county-expand.cgi [new file with mode: 0755]
httemplate/edit/process/cust_main_county.cgi [new file with mode: 0755]
httemplate/edit/process/cust_pay.cgi [new file with mode: 0755]
httemplate/edit/process/cust_pkg.cgi [new file with mode: 0755]
httemplate/edit/process/domain_record.cgi [new file with mode: 0755]
httemplate/edit/process/generic.cgi [new file with mode: 0644]
httemplate/edit/process/msgcat.cgi [new file with mode: 0644]
httemplate/edit/process/part_bill_event.cgi [new file with mode: 0755]
httemplate/edit/process/part_export.cgi [new file with mode: 0644]
httemplate/edit/process/part_pkg.cgi [new file with mode: 0755]
httemplate/edit/process/part_referral.cgi [new file with mode: 0755]
httemplate/edit/process/part_svc.cgi [new file with mode: 0755]
httemplate/edit/process/quick-charge.cgi [new file with mode: 0644]
httemplate/edit/process/quick-cust_pkg.cgi [new file with mode: 0644]
httemplate/edit/process/router.cgi [new file with mode: 0644]
httemplate/edit/process/svc_acct.cgi [new file with mode: 0755]
httemplate/edit/process/svc_acct_pop.cgi [new file with mode: 0755]
httemplate/edit/process/svc_broadband.cgi [new file with mode: 0644]
httemplate/edit/process/svc_domain.cgi [new file with mode: 0755]
httemplate/edit/process/svc_forward.cgi [new file with mode: 0755]
httemplate/edit/process/svc_www.cgi [new file with mode: 0644]
httemplate/edit/router.cgi [new file with mode: 0755]
httemplate/edit/svc_acct.cgi [new file with mode: 0755]
httemplate/edit/svc_acct_pop.cgi [new file with mode: 0755]
httemplate/edit/svc_broadband.cgi [new file with mode: 0644]
httemplate/edit/svc_domain.cgi [new file with mode: 0755]
httemplate/edit/svc_forward.cgi [new file with mode: 0755]
httemplate/edit/svc_www.cgi [new file with mode: 0644]
httemplate/graph/money_time-graph.cgi [new file with mode: 0755]
httemplate/graph/money_time.cgi [new file with mode: 0644]
httemplate/images/mid-logo.png [new file with mode: 0644]
httemplate/images/small-logo.png [new file with mode: 0644]
httemplate/index.html [new file with mode: 0644]
httemplate/misc/bill.cgi [new file with mode: 0755]
httemplate/misc/cancel-unaudited.cgi [new file with mode: 0755]
httemplate/misc/cancel_pkg.cgi [new file with mode: 0755]
httemplate/misc/catchall.cgi [new file with mode: 0755]
httemplate/misc/change_pkg.cgi [new file with mode: 0755]
httemplate/misc/cust_main-cancel.cgi [new file with mode: 0755]
httemplate/misc/cust_main-import.cgi [new file with mode: 0644]
httemplate/misc/cust_main-import_charges.cgi [new file with mode: 0644]
httemplate/misc/delete-cust_pay.cgi [new file with mode: 0755]
httemplate/misc/delete-customer.cgi [new file with mode: 0755]
httemplate/misc/delete-domain_record.cgi [new file with mode: 0755]
httemplate/misc/delete-part_export.cgi [new file with mode: 0755]
httemplate/misc/expire_pkg.cgi [new file with mode: 0755]
httemplate/misc/link.cgi [new file with mode: 0755]
httemplate/misc/meta-import.cgi [new file with mode: 0644]
httemplate/misc/print-invoice.cgi [new file with mode: 0755]
httemplate/misc/process/catchall.cgi [new file with mode: 0755]
httemplate/misc/process/cust_main-import.cgi [new file with mode: 0644]
httemplate/misc/process/cust_main-import_charges.cgi [new file with mode: 0644]
httemplate/misc/process/delete-customer.cgi [new file with mode: 0755]
httemplate/misc/process/link.cgi [new file with mode: 0755]
httemplate/misc/process/meta-import.cgi [new file with mode: 0644]
httemplate/misc/queue.cgi [new file with mode: 0644]
httemplate/misc/susp_pkg.cgi [new file with mode: 0755]
httemplate/misc/unapply-cust_pay.cgi [new file with mode: 0755]
httemplate/misc/unprovision.cgi [new file with mode: 0755]
httemplate/misc/unsusp_pkg.cgi [new file with mode: 0755]
httemplate/search/cust_bill.cgi [new file with mode: 0755]
httemplate/search/cust_bill.html [new file with mode: 0755]
httemplate/search/cust_bill_event.cgi [new file with mode: 0644]
httemplate/search/cust_bill_event.html [new file with mode: 0755]
httemplate/search/cust_main-otaker.cgi [new file with mode: 0755]
httemplate/search/cust_main-payinfo.html [new file with mode: 0755]
httemplate/search/cust_main-quickpay.html [new file with mode: 0755]
httemplate/search/cust_main.cgi [new file with mode: 0755]
httemplate/search/cust_main.html [new file with mode: 0755]
httemplate/search/cust_pay.cgi [new file with mode: 0755]
httemplate/search/cust_pay.html [new file with mode: 0755]
httemplate/search/cust_pkg.cgi [new file with mode: 0755]
httemplate/search/cust_pkg.html [new file with mode: 0755]
httemplate/search/report_cc.cgi [new file with mode: 0755]
httemplate/search/report_cc.html [new file with mode: 0755]
httemplate/search/report_credit.cgi [new file with mode: 0755]
httemplate/search/report_credit.html [new file with mode: 0755]
httemplate/search/report_cust_pay.html [new file with mode: 0644]
httemplate/search/report_receivables.cgi [new file with mode: 0755]
httemplate/search/report_tax.cgi [new file with mode: 0755]
httemplate/search/report_tax.html [new file with mode: 0755]
httemplate/search/sql.cgi [new file with mode: 0755]
httemplate/search/svc_acct.cgi [new file with mode: 0755]
httemplate/search/svc_acct.html [new file with mode: 0755]
httemplate/search/svc_domain.cgi [new file with mode: 0755]
httemplate/search/svc_domain.html [new file with mode: 0755]
httemplate/view/cust_bill.cgi [new file with mode: 0755]
httemplate/view/cust_main.cgi [new file with mode: 0755]
httemplate/view/cust_pkg.cgi [new file with mode: 0755]
httemplate/view/svc_acct.cgi [new file with mode: 0755]
httemplate/view/svc_broadband.cgi [new file with mode: 0644]
httemplate/view/svc_domain.cgi [new file with mode: 0755]
httemplate/view/svc_forward.cgi [new file with mode: 0755]
httemplate/view/svc_www.cgi [new file with mode: 0644]
init.d/freeside-init [new file with mode: 0644]
install/freebsd/INSTALL [new file with mode: 0755]
install/freebsd/ports [new file with mode: 0644]
install/redhat/7.3/INSTALL [new file with mode: 0644]
install/redhat/7.3/sources.list [new file with mode: 0644]
rt/COPYING [new file with mode: 0755]
rt/ChangeLog [new file with mode: 0644]
rt/Makefile [new file with mode: 0644]
rt/README [new file with mode: 0755]
rt/TODO [new file with mode: 0755]
rt/bin/initacls.Oracle [new file with mode: 0644]
rt/bin/initacls.Pg [new file with mode: 0755]
rt/bin/initacls.mysql [new file with mode: 0755]
rt/bin/mason_handler.fcgi [new file with mode: 0755]
rt/bin/mason_handler.scgi [new file with mode: 0755]
rt/bin/rt [new file with mode: 0755]
rt/bin/rt-mailgate [new file with mode: 0755]
rt/bin/rtadmin [new file with mode: 0644]
rt/bin/webmux.pl [new file with mode: 0755]
rt/docs/README.docs [new file with mode: 0755]
rt/docs/Security [new file with mode: 0644]
rt/docs/design_docs/CARS [new file with mode: 0755]
rt/docs/design_docs/TransactionTypes.txt [new file with mode: 0755]
rt/docs/design_docs/acls [new file with mode: 0644]
rt/docs/design_docs/basic-definitions.txt [new file with mode: 0644]
rt/docs/design_docs/cli_spec [new file with mode: 0644]
rt/docs/design_docs/cvs_integration [new file with mode: 0644]
rt/docs/design_docs/evil_plans [new file with mode: 0644]
rt/docs/design_docs/link-definitions.txt [new file with mode: 0644]
rt/docs/design_docs/local_hacking [new file with mode: 0644]
rt/docs/design_docs/subscription-definitions.txt [new file with mode: 0755]
rt/docs/design_docs/users [new file with mode: 0644]
rt/docs/rt.gif [new file with mode: 0755]
rt/etc/acl.Oracle [new file with mode: 0644]
rt/etc/acl.Pg [new file with mode: 0755]
rt/etc/acl.mysql [new file with mode: 0755]
rt/etc/config.pm [new file with mode: 0755]
rt/etc/rt.spec [new file with mode: 0644]
rt/etc/schema.Oracle [new file with mode: 0644]
rt/etc/schema.Pg [new file with mode: 0755]
rt/etc/schema.mysql [new file with mode: 0755]
rt/etc/schema.pm [new file with mode: 0644]
rt/lib/MANIFEST [new file with mode: 0644]
rt/lib/MANIFEST.SKIP [new file with mode: 0644]
rt/lib/Makefile.PL [new file with mode: 0644]
rt/lib/RT.pm [new file with mode: 0644]
rt/lib/RT/ACE.pm [new file with mode: 0755]
rt/lib/RT/ACL.pm [new file with mode: 0755]
rt/lib/RT/Action/Autoreply.pm [new file with mode: 0755]
rt/lib/RT/Action/Generic.pm [new file with mode: 0755]
rt/lib/RT/Action/Notify.pm [new file with mode: 0755]
rt/lib/RT/Action/NotifyAsComment.pm [new file with mode: 0755]
rt/lib/RT/Action/OpenDependent.pm [new file with mode: 0644]
rt/lib/RT/Action/ResolveMembers.pm [new file with mode: 0644]
rt/lib/RT/Action/SendEmail.pm [new file with mode: 0755]
rt/lib/RT/Action/SendPasswordEmail.pm [new file with mode: 0755]
rt/lib/RT/Action/StallDependent.pm [new file with mode: 0644]
rt/lib/RT/Attachment.pm [new file with mode: 0755]
rt/lib/RT/Attachments.pm [new file with mode: 0755]
rt/lib/RT/Condition/AnyTransaction.pm [new file with mode: 0644]
rt/lib/RT/Condition/Generic.pm [new file with mode: 0755]
rt/lib/RT/Condition/NewDependency.pm [new file with mode: 0644]
rt/lib/RT/Condition/StatusChange.pm [new file with mode: 0644]
rt/lib/RT/CurrentUser.pm [new file with mode: 0755]
rt/lib/RT/Date.pm [new file with mode: 0644]
rt/lib/RT/EasySearch.pm [new file with mode: 0755]
rt/lib/RT/Group.pm [new file with mode: 0755]
rt/lib/RT/GroupMember.pm [new file with mode: 0755]
rt/lib/RT/GroupMembers.pm [new file with mode: 0755]
rt/lib/RT/Groups.pm [new file with mode: 0755]
rt/lib/RT/Handle.pm [new file with mode: 0644]
rt/lib/RT/Interface/CLI.pm [new file with mode: 0644]
rt/lib/RT/Interface/Email.pm [new file with mode: 0755]
rt/lib/RT/Interface/Web.pm [new file with mode: 0644]
rt/lib/RT/Keyword.pm [new file with mode: 0644]
rt/lib/RT/KeywordSelect.pm [new file with mode: 0644]
rt/lib/RT/KeywordSelects.pm [new file with mode: 0644]
rt/lib/RT/Keywords.pm [new file with mode: 0644]
rt/lib/RT/Link.pm [new file with mode: 0644]
rt/lib/RT/Links.pm [new file with mode: 0644]
rt/lib/RT/ObjectKeyword.pm [new file with mode: 0644]
rt/lib/RT/ObjectKeywords.pm [new file with mode: 0644]
rt/lib/RT/Queue.pm [new file with mode: 0755]
rt/lib/RT/Queues.pm [new file with mode: 0755]
rt/lib/RT/Record.pm [new file with mode: 0755]
rt/lib/RT/Scrip.pm [new file with mode: 0755]
rt/lib/RT/ScripAction.pm [new file with mode: 0755]
rt/lib/RT/ScripActions.pm [new file with mode: 0755]
rt/lib/RT/ScripCondition.pm [new file with mode: 0755]
rt/lib/RT/ScripConditions.pm [new file with mode: 0755]
rt/lib/RT/Scrips.pm [new file with mode: 0755]
rt/lib/RT/Template.pm [new file with mode: 0755]
rt/lib/RT/Templates.pm [new file with mode: 0755]
rt/lib/RT/TestHarness.pm [new file with mode: 0644]
rt/lib/RT/Ticket.pm [new file with mode: 0755]
rt/lib/RT/Tickets.pm [new file with mode: 0755]
rt/lib/RT/Transaction.pm [new file with mode: 0755]
rt/lib/RT/Transactions.pm [new file with mode: 0755]
rt/lib/RT/User.pm [new file with mode: 0755]
rt/lib/RT/Users.pm [new file with mode: 0755]
rt/lib/RT/Watcher.pm [new file with mode: 0755]
rt/lib/RT/Watchers.pm [new file with mode: 0755]
rt/lib/test.pl [new file with mode: 0644]
rt/tools/cpan2rpm [new file with mode: 0644]
rt/tools/initdb [new file with mode: 0644]
rt/tools/insertdata [new file with mode: 0755]
rt/tools/testdeps [new file with mode: 0644]
rt/webrt/Admin/Elements/CreateQueueCalled [new file with mode: 0755]
rt/webrt/Admin/Elements/CreateUserCalled [new file with mode: 0755]
rt/webrt/Admin/Elements/EditUserComments [new file with mode: 0755]
rt/webrt/Admin/Elements/GrantQueueRightsTo [new file with mode: 0755]
rt/webrt/Admin/Elements/GroupTabs [new file with mode: 0755]
rt/webrt/Admin/Elements/Header [new file with mode: 0755]
rt/webrt/Admin/Elements/ListGlobalKeywordSelects [new file with mode: 0644]
rt/webrt/Admin/Elements/ListGlobalScrips [new file with mode: 0755]
rt/webrt/Admin/Elements/ModifyKeyword [new file with mode: 0644]
rt/webrt/Admin/Elements/ModifyKeywordSelect [new file with mode: 0644]
rt/webrt/Admin/Elements/ModifyQueue [new file with mode: 0755]
rt/webrt/Admin/Elements/ModifyTemplate [new file with mode: 0755]
rt/webrt/Admin/Elements/ModifyUser [new file with mode: 0755]
rt/webrt/Admin/Elements/QueueRightsForUser [new file with mode: 0644]
rt/webrt/Admin/Elements/QueueTabs [new file with mode: 0755]
rt/webrt/Admin/Elements/SelectKeywordSelect [new file with mode: 0644]
rt/webrt/Admin/Elements/SelectModifyGroup [new file with mode: 0644]
rt/webrt/Admin/Elements/SelectModifyKeyword [new file with mode: 0644]
rt/webrt/Admin/Elements/SelectModifyKeywordSelect [new file with mode: 0644]
rt/webrt/Admin/Elements/SelectModifyQueue [new file with mode: 0755]
rt/webrt/Admin/Elements/SelectModifyUser [new file with mode: 0755]
rt/webrt/Admin/Elements/SelectQueueRights [new file with mode: 0755]
rt/webrt/Admin/Elements/SelectRights [new file with mode: 0644]
rt/webrt/Admin/Elements/SelectScrip [new file with mode: 0755]
rt/webrt/Admin/Elements/SelectScripAction [new file with mode: 0644]
rt/webrt/Admin/Elements/SelectScripCondition [new file with mode: 0644]
rt/webrt/Admin/Elements/SelectSingleOrMultiple [new file with mode: 0644]
rt/webrt/Admin/Elements/SelectTemplate [new file with mode: 0755]
rt/webrt/Admin/Elements/SelectUsers [new file with mode: 0644]
rt/webrt/Admin/Elements/SystemTabs [new file with mode: 0755]
rt/webrt/Admin/Elements/Tabs [new file with mode: 0755]
rt/webrt/Admin/Elements/UserTabs [new file with mode: 0755]
rt/webrt/Admin/Global/GroupRights.html [new file with mode: 0755]
rt/webrt/Admin/Global/Keywords.html [new file with mode: 0644]
rt/webrt/Admin/Global/Scrips.html [new file with mode: 0755]
rt/webrt/Admin/Global/Template.html [new file with mode: 0755]
rt/webrt/Admin/Global/Templates.html [new file with mode: 0755]
rt/webrt/Admin/Global/UserRights.html [new file with mode: 0755]
rt/webrt/Admin/Global/index.html [new file with mode: 0755]
rt/webrt/Admin/Groups/Members.html [new file with mode: 0644]
rt/webrt/Admin/Groups/Modify.html [new file with mode: 0644]
rt/webrt/Admin/Groups/Rights.html [new file with mode: 0644]
rt/webrt/Admin/Groups/index.html [new file with mode: 0644]
rt/webrt/Admin/KeywordSelects/Modify.html [new file with mode: 0644]
rt/webrt/Admin/KeywordSelects/index.html [new file with mode: 0644]
rt/webrt/Admin/Keywords/Modify.html [new file with mode: 0644]
rt/webrt/Admin/Keywords/index.html [new file with mode: 0644]
rt/webrt/Admin/Queues/Create.html [new file with mode: 0755]
rt/webrt/Admin/Queues/GroupRights.html [new file with mode: 0755]
rt/webrt/Admin/Queues/Keywords.html [new file with mode: 0644]
rt/webrt/Admin/Queues/Modify.html [new file with mode: 0755]
rt/webrt/Admin/Queues/People.html [new file with mode: 0755]
rt/webrt/Admin/Queues/Scrips.html [new file with mode: 0755]
rt/webrt/Admin/Queues/Template.html [new file with mode: 0755]
rt/webrt/Admin/Queues/Templates.html [new file with mode: 0755]
rt/webrt/Admin/Queues/UserRights.html [new file with mode: 0755]
rt/webrt/Admin/Queues/index.html [new file with mode: 0755]
rt/webrt/Admin/Users/Modify.html [new file with mode: 0755]
rt/webrt/Admin/Users/Prefs.html [new file with mode: 0755]
rt/webrt/Admin/Users/Rights.html [new file with mode: 0644]
rt/webrt/Admin/Users/index.html [new file with mode: 0755]
rt/webrt/Admin/index.html [new file with mode: 0755]
rt/webrt/Elements/Checkbox [new file with mode: 0755]
rt/webrt/Elements/CreateTicket [new file with mode: 0644]
rt/webrt/Elements/CustomHomepageHeader [new file with mode: 0644]
rt/webrt/Elements/Error [new file with mode: 0755]
rt/webrt/Elements/Footer [new file with mode: 0755]
rt/webrt/Elements/GotoTicket [new file with mode: 0644]
rt/webrt/Elements/Header [new file with mode: 0755]
rt/webrt/Elements/ListActions [new file with mode: 0755]
rt/webrt/Elements/Login [new file with mode: 0755]
rt/webrt/Elements/MessageBox [new file with mode: 0644]
rt/webrt/Elements/MyRequests [new file with mode: 0644]
rt/webrt/Elements/MyTickets [new file with mode: 0644]
rt/webrt/Elements/Quicksearch [new file with mode: 0644]
rt/webrt/Elements/Refresh [new file with mode: 0644]
rt/webrt/Elements/Section [new file with mode: 0755]
rt/webrt/Elements/SelectBoolean [new file with mode: 0755]
rt/webrt/Elements/SelectDate [new file with mode: 0755]
rt/webrt/Elements/SelectDateRelation [new file with mode: 0755]
rt/webrt/Elements/SelectDateType [new file with mode: 0755]
rt/webrt/Elements/SelectEqualityOperator [new file with mode: 0755]
rt/webrt/Elements/SelectKeyword [new file with mode: 0644]
rt/webrt/Elements/SelectKeywordOptions [new file with mode: 0644]
rt/webrt/Elements/SelectLinkType [new file with mode: 0644]
rt/webrt/Elements/SelectMatch [new file with mode: 0644]
rt/webrt/Elements/SelectNewTicketQueue [new file with mode: 0755]
rt/webrt/Elements/SelectOwner [new file with mode: 0755]
rt/webrt/Elements/SelectQueue [new file with mode: 0755]
rt/webrt/Elements/SelectResultsPerPage [new file with mode: 0644]
rt/webrt/Elements/SelectSortOrder [new file with mode: 0644]
rt/webrt/Elements/SelectStatus [new file with mode: 0755]
rt/webrt/Elements/SelectTicketSortBy [new file with mode: 0644]
rt/webrt/Elements/SelectUsers [new file with mode: 0755]
rt/webrt/Elements/SelectWatcherType [new file with mode: 0644]
rt/webrt/Elements/ShadedBox [new file with mode: 0755]
rt/webrt/Elements/Submit [new file with mode: 0755]
rt/webrt/Elements/Tabs [new file with mode: 0755]
rt/webrt/Elements/TitleBoxEnd [new file with mode: 0755]
rt/webrt/Elements/TitleBoxStart [new file with mode: 0755]
rt/webrt/Elements/ViewUser [new file with mode: 0644]
rt/webrt/Elements/dayMenu [new file with mode: 0755]
rt/webrt/Elements/monthMenu [new file with mode: 0755]
rt/webrt/Elements/yearMenu [new file with mode: 0755]
rt/webrt/NoAuth/Logout.html [new file with mode: 0755]
rt/webrt/NoAuth/Reminder.html [new file with mode: 0755]
rt/webrt/NoAuth/images/rt.jpg [new file with mode: 0644]
rt/webrt/NoAuth/images/spacer.gif [new file with mode: 0644]
rt/webrt/NoAuth/webrt.css [new file with mode: 0755]
rt/webrt/Search/Bulk.html [new file with mode: 0755]
rt/webrt/Search/Listing.html [new file with mode: 0755]
rt/webrt/Search/PickRestriction [new file with mode: 0755]
rt/webrt/Search/RestrictSearch.html [new file with mode: 0755]
rt/webrt/Search/TicketCell [new file with mode: 0644]
rt/webrt/SelfService/Attachment/dhandler [new file with mode: 0644]
rt/webrt/SelfService/Closed.html [new file with mode: 0644]
rt/webrt/SelfService/Create.html [new file with mode: 0755]
rt/webrt/SelfService/Display.html [new file with mode: 0755]
rt/webrt/SelfService/Elements/GotoTicket [new file with mode: 0755]
rt/webrt/SelfService/Elements/Header [new file with mode: 0755]
rt/webrt/SelfService/Elements/MyRequests [new file with mode: 0644]
rt/webrt/SelfService/Elements/Tabs [new file with mode: 0644]
rt/webrt/SelfService/Error.html [new file with mode: 0755]
rt/webrt/SelfService/Prefs.html [new file with mode: 0755]
rt/webrt/SelfService/Update.html [new file with mode: 0755]
rt/webrt/SelfService/index.html [new file with mode: 0644]
rt/webrt/Ticket/Attachment/dhandler [new file with mode: 0644]
rt/webrt/Ticket/Create.html [new file with mode: 0755]
rt/webrt/Ticket/Display.html [new file with mode: 0755]
rt/webrt/Ticket/Elements/AddWatchers [new file with mode: 0755]
rt/webrt/Ticket/Elements/EditBasics [new file with mode: 0755]
rt/webrt/Ticket/Elements/EditDates [new file with mode: 0755]
rt/webrt/Ticket/Elements/EditKeywordSelects [new file with mode: 0644]
rt/webrt/Ticket/Elements/EditLinks [new file with mode: 0755]
rt/webrt/Ticket/Elements/EditPeople [new file with mode: 0755]
rt/webrt/Ticket/Elements/EditWatchers [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowBasics [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowDates [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowDependencies [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowHistory [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowKeywordSelects [new file with mode: 0644]
rt/webrt/Ticket/Elements/ShowLinks [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowMemberOf [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowMembers [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowPeople [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowReferences [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowRequestor [new file with mode: 0644]
rt/webrt/Ticket/Elements/ShowSummary [new file with mode: 0755]
rt/webrt/Ticket/Elements/ShowTransaction [new file with mode: 0755]
rt/webrt/Ticket/Elements/Tabs [new file with mode: 0755]
rt/webrt/Ticket/Elements/ToolBar [new file with mode: 0755]
rt/webrt/Ticket/History.html [new file with mode: 0755]
rt/webrt/Ticket/Modify.html [new file with mode: 0755]
rt/webrt/Ticket/ModifyAll.html [new file with mode: 0755]
rt/webrt/Ticket/ModifyDates.html [new file with mode: 0755]
rt/webrt/Ticket/ModifyLinks.html [new file with mode: 0755]
rt/webrt/Ticket/ModifyPeople.html [new file with mode: 0755]
rt/webrt/Ticket/Update.html [new file with mode: 0755]
rt/webrt/User/Prefs.html [new file with mode: 0755]
rt/webrt/autohandler [new file with mode: 0755]
rt/webrt/index.html [new file with mode: 0644]
site_perl/Bill.pm [deleted file]
site_perl/CGI.pm [deleted file]
site_perl/Conf.pm [deleted file]
site_perl/Invoice.pm [deleted file]
site_perl/Record.pm [deleted file]
site_perl/SSH.pm [deleted file]
site_perl/UID.pm [deleted file]
site_perl/agent.pm [deleted file]
site_perl/agent_type.pm [deleted file]
site_perl/cust_bill.pm [deleted file]
site_perl/cust_bill_pkg.pm [deleted file]
site_perl/cust_credit.pm [deleted file]
site_perl/cust_main.pm [deleted file]
site_perl/cust_main_county.pm [deleted file]
site_perl/cust_pay.pm [deleted file]
site_perl/cust_pkg.pm [deleted file]
site_perl/cust_refund.pm [deleted file]
site_perl/cust_svc.pm [deleted file]
site_perl/dbdef.pm [deleted file]
site_perl/dbdef_colgroup.pm [deleted file]
site_perl/dbdef_column.pm [deleted file]
site_perl/dbdef_index.pm [deleted file]
site_perl/dbdef_table.pm [deleted file]
site_perl/dbdef_unique.pm [deleted file]
site_perl/part_pkg.pm [deleted file]
site_perl/part_referral.pm [deleted file]
site_perl/part_svc.pm [deleted file]
site_perl/pkg_svc.pm [deleted file]
site_perl/svc_acct.pm [deleted file]
site_perl/svc_acct_pop.pm [deleted file]
site_perl/svc_acct_sm.pm [deleted file]
site_perl/svc_domain.pm [deleted file]
site_perl/table_template-svc.pm [deleted file]
site_perl/table_template-unique.pm [deleted file]
site_perl/table_template.pm [deleted file]
site_perl/type_pkgs.pm [deleted file]
test/cgi-test [new file with mode: 0755]

diff --git a/Artistic b/Artistic
deleted file mode 100644 (file)
index 4ffc78e..0000000
--- a/Artistic
+++ /dev/null
@@ -1,125 +0,0 @@
-                      The "Artistic License"
-
-                             Preamble
-
-The intent of this document is to state the conditions under which a
-Package may be copied, such that the Copyright Holder maintains some
-semblance of artistic control over the development of the Package,
-while giving the users of the package the right to use and distribute
-the Package in a more-or-less customary fashion, plus the right to make
-reasonable modifications.
-
-It also grants you the rights to reuse parts of a Package in your own
-programs without transferring this License to those programs, provided
-that you meet some reasonable requirements.
-
-Definitions:
-
-        "Package" refers to the collection of files distributed by the
-        Copyright Holder, and derivatives of that collection of files
-        created through textual modification.
-
-        "Standard Version" refers to such a Package if it has not been
-        modified, or has been modified in accordance with the wishes
-        of the Copyright Holder as specified below.
-
-        "Copyright Holder" is whoever is named in the copyright or
-        copyrights for the package.
-
-        "You" is you, if you're thinking about copying or distributing
-        this Package.
-
-        "Reasonable copying fee" is whatever you can justify on the
-        basis of media cost, duplication charges, time of people involved,
-        and so on.  (You will not be required to justify it to the
-        Copyright Holder, but only to the computing community at large
-        as a market that must bear the fee.)
-
-        "Freely Available" means that no fee is charged for the item
-        itself, though there may be fees involved in handling the item.
-        It also means that recipients of the item may redistribute it
-        under the same conditions they received it.
-
-1. You may make and give away verbatim copies of the source form of the
-Standard Version of this Package without restriction, provided that you
-duplicate all of the original copyright notices and associated disclaimers.
-
-2. You may apply bug fixes, portability fixes and other modifications
-derived from the Public Domain or from the Copyright Holder.  A Package
-modified in such a way shall still be considered the Standard Version.
-
-3. You may otherwise modify your copy of this Package in any way, provided
-that you insert a prominent notice in each changed file stating how and
-when you changed that file, and provided that you do at least ONE of the
-following:
-
-    a) place your modifications in the Public Domain or otherwise make them
-    Freely Available, such as by posting said modifications to Usenet or
-    an equivalent medium, or placing the modifications on a major archive
-    site such as uunet.uu.net, or by allowing the Copyright Holder to include
-    your modifications in the Standard Version of the Package.
-
-    b) use the modified Package only within your corporation or organization.
-
-    c) rename any non-standard executables so the names do not conflict
-    with standard executables, which must also be provided, and provide
-    a separate manual page for each non-standard executable that clearly
-    documents how it differs from the Standard Version.
-
-    d) make other distribution arrangements with the Copyright Holder.
-
-4. You may distribute the programs of this Package in object code or
-executable form, provided that you do at least ONE of the following:
-
-    a) distribute a Standard Version of the executables and library files,
-    together with instructions (in the manual page or equivalent) on where
-    to get the Standard Version.
-
-    b) accompany the distribution with the machine-readable source of
-    the Package with your modifications.
-
-    c) give non-standard executables non-standard names, and clearly
-    document the differences in manual pages (or equivalent), together
-    with instructions on where to get the Standard Version.
-
-    d) make other distribution arrangements with the Copyright Holder.
-
-5. You may charge a reasonable copying fee for any distribution of this
-Package.  You may charge any fee you choose for support of this
-Package.  You may not charge a fee for this Package itself.  However,
-you may distribute this Package in aggregate with other (possibly
-commercial) programs as part of a larger (possibly commercial) software
-distribution provided that you do not advertise this Package as a
-product of your own.
-
-6. The scripts and library files supplied as input to or produced as
-output from the programs of this Package do not automatically fall
-under the copyright of this Package, but belong to whomever generated
-them, and may be sold commercially, and may be aggregated with this
-Package.  If such scripts or library files are aggregated with this
-Package via the so-called "undump" or "unexec" methods of producing a
-binary executable image, then distribution of such an image shall
-neither be construed as a distribution of this Package nor shall it
-fall under the restrictions of Paragraphs 3 and 4, provided that you do
-not represent such an executable image as a Standard Version of this
-Package.
-
-7. You may reuse parts of this Package in your own programs, provided that
-you explicitly state where you got them from, in the source code (and, left
-to your courtesy, in the documentation), duplicating all the associated
-copyright notices and disclaimers. Besides your changes, if any, must be
-clearly marked as such. Parts reused that way will no longer fall under this
-license if, and only if, the name of your program(s) have no immediate
-connection with the name of the Package itself or its associated programs.
-You may then apply whatever restrictions you wish on the reused parts or
-choose to place them in the Public Domain--this will apply only within the
-context of your package.
-
-8. The name of the Copyright Holder may not be used to endorse or promote
-products derived from this software without specific prior written permission.
-
-9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
-IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
-WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
-
-                                The End
diff --git a/CREDITS b/CREDITS
index 87c79a7..0b4e2d9 100644 (file)
--- a/CREDITS
+++ b/CREDITS
 Thanks to Matt Simerson <matt@michweb.net> of MichWeb Inc. for documentation
-and pre-release testing.  Without his help the documentation in the first
+and pre-release testing.  Without his help the documentation in 1.0.0
 release would have consisted of a single screenfull of text.
+(To clear up some misunderstanding, Matt did not write the current
+documentation.)
 
-# Steve Cleff <cleff@yahoo.com> did the default background image and is also
-# the creator of Freeside's mascot, Snakeman.
+Steve Cleff <cleff@yahoo.com> did the default background image in 1.0.x and
+is also the creator of Freeside's elusive mascot, Snakeman, who we hope will
+make an appearance in an upcoming version.
 
-Jerry St. Pierre <jstpi@city.timmins.on.ca> did the "SISD" graphic.
+Jerry St. Pierre <jstpi@city.timmins.on.ca> did the "SISD" graphic used in
+1.0.x and most of 1.1.x.
+
+Mark Norris of Urban Design, Inc. <http://www.urban.com/> did the red "S"
+logo for later 1.1.x versions and 1.2.x
 
 Brian McCane? <bmccane@maxbaud.net> contributed PostgreSQL support, HTML
 style enhancements and many, many bugfixes.
 
-Everything else is my (Ivan Kohler <ivan@sisd.com>) fault.
+Cerkit <cerkit@alfheim.net> contributed rsync support and desynced hosts.
+His changes will hopefully be included in an upcoming version.
+
+CompleteHOST, Inc. (http://www.completehost.com) funded the development of the
+following features:
+  - Multiple, separate databases and configurations on one box.
+  - Per-customer pricing (custom packages)
+  - Internationalization wrt addresses (cust_main, cust_main_county)
+Thanks!
+
+Mark Williamson <mark.williamson@ebbs.com.au> and Roger Mangraviti
+<rem@atu.com.au> contributed state/provence listings for Australia.
+
+Peter Wemm <peter@netplex.com.au> sent in a bunch of bugfixes for the 1.2
+release.
+
+Greg Kuhnert <gregk@no1.com.au> sent some documentation updates.
+
+Joel Griffiths <griff@aver-computer.com> contribued many bugfixes as well as
+the print-batch script.
+
+NetLoud <http://www.netloud.com/> funded the development of the following
+features:
+  - IEAK support for the signup server
+  - Pre-payment support
+
+NetAcces.Net (not netaccess.net) funded the development of the following
+features:
+  - DNS tracking and export to BIND configuration files
+  - Web site virtual host tracking and export to Apache configuration files
+
+Kristian Hoffmann <khoff@pc-intouch.com> contributed Netscape CCK
+autoconfiguration support for the signup server, lots of great mailing
+lists posts which I shamelessly made into documentation, fixes to get rid of
+the embarassing and non-database-normal "owed" field, and many other things
+I'm forgetting.  Most recently Kristian and Mark (last name?) contributed
+the IP address tracking and svc_broadband in 1.5.
+
+Jeff Finucane <jeff@cmh.net> send in a bunch of bugfixes (for the sendmail
+export, cancel-unaudited.cgi), patches to support billing date modification,
+and probably other things too (sorry if I forgot them).  And yet even more
+bug squashing, thanks!  *and* he single-handedly implemented all the necessary
+work to get rid of svc_acct_sm and the "default domain"  thanks!!  and rewrote
+the financials!  wow, thanks jeff!  and contributed financial reports!
+
+Kenny Elliott <kenny@neoserve.com> contributed ICRADIUS radreply table support,
+allowing attributes with ICRADIUS, helped fix many bugs, and some
+other stuff I can't recall (sorry).
+
+Stephen Amadei <amadei@dandy.net> contribued portability cleanups for the
+low-level DBI stuff.
+
+Jason Spence <thalakan@frys.com> contributed admin.html and other
+documentation, autocapnames javascript, bugfixes & other neat stuff I can't
+remember.
+
+Brad Dameron <bdameron@tscnet.com> contributed code to do configurable state
+and referral defaults.
+
+Surf and Sip, Inc., <http://www.surfandsip.com> sponsored a long-requested
+feature - the session monitor and time-based prepaid cards.
+Matt Peterson <matt@peterson.org> and Mack ? <mackn@mackn.net> tested
+the new features and contributed many bugfixes.
+
+Landel Telecom <http://www.landel.com/> sponsored shipping addresses and
+customer notes, as well as an update of the CP provisioning.
+
+nikotel, Inc. <http://www.nikojet.com> sponsored the inclusion of
+customer-to-customer referrals in the web interface and signup server.
+
+Three Bubba's Innanet <http://www.inna.net> sponsored expedited check entry,
+the "similar names warning" feature, and a number of other enhancements.
+
+Dave Burgess <burgess@neonramp.com> sent in a bunch of fixes and small changes
+and will doubtless send more once he's got his tree under control.
+
+Luke Pfeifer <freeside@globalli.com> contributed the "subscription" price plan.
+
+Noment Networks, LLC <http://www.noment.com/> sponsored ICRADIUS/FreeRADIUS
+groups, message catalogs, and signup server enhancements.
+
+Donald Greer <dgreer@austintx.com> provided the SQL to work around MySQL's lack
+of subqueries, and Dale Hege <fhege@lumenexus.net> provided the patches.
+Thanks!
+
+<baloo@gimpgirl.com> sent in several documentation patches.
+
+"Stephen Bechard" <steve@destek.net> sent in patches for svc_www services and
+other fixes.
+
+Charles A Beasley <cbeasley@noment.net> contributed quota editing for the
+Infostreet export.
+
+Richard Siddall <richard.siddall@elirion.net> sent in Mason fixes and other
+things I'm probably forgetting.
+
+Everything else is my (Ivan Kohler <ivan@420.am>) fault.
 
diff --git a/FS/Changes b/FS/Changes
new file mode 100644 (file)
index 0000000..c94ef10
--- /dev/null
@@ -0,0 +1,5 @@
+Revision history for Perl extension FS.
+
+0.01  Wed Aug  4 00:13:45 1999
+       - original version; created by h2xs 1.19
+
diff --git a/FS/FS.pm b/FS/FS.pm
new file mode 100644 (file)
index 0000000..e4a3208
--- /dev/null
+++ b/FS/FS.pm
@@ -0,0 +1,231 @@
+package FS;
+
+use strict;
+use vars qw($VERSION);
+
+$VERSION = '0.01';
+
+#find missing entries in this file with:
+# for a in `ls *pm | cut -d. -f1`; do grep 'L<FS::'$a'>' ../FS.pm >/dev/null || echo "missing $a" ; done
+
+1;
+__END__
+
+=head1 NAME
+
+FS - Freeside Perl modules
+
+=head1 SYNOPSIS
+
+Freeside perl modules and CLI utilities.
+
+=head2 Utility classes
+
+L<FS::Conf> - Freeside configuration values
+
+L<FS::ConfItem> - Freeside configuration option meta-data.
+
+L<FS::UID> - User class (not yet OO)
+
+L<FS::CGI> - Non OO-subroutines for the web interface.
+
+L<FS::Msgcat> - Message catalog
+
+L<FS::SearchCache> - Search cache
+
+L<FS::raddb> - RADIUS dictionary
+
+=head2 Database record classes
+
+L<FS::Record> - Database record base class
+
+L<FS::svc_acct_pop> - POP (Point of Presence, not Post
+Office Protocol) class
+
+L<FS::part_pop_local> - Local calling area class
+
+L<FS::part_referral> - Referral class
+
+L<FS::cust_main_county> - Locale (tax rate) class
+
+L<FS::cust_tax_exempt> - Tax exemption record class
+
+L<FS::svc_Common> - Service base class
+
+L<FS::svc_acct> - Account (shell, RADIUS, POP3) class
+
+L<FS::radius_usergroup> - RADIUS groups
+
+L<FS::svc_domain> - Domain class
+
+L<FS::domain_record> - DNS zone entries
+
+L<FS::svc_forward> - Mail forwarding class
+
+L<FS::svc_www> - Web virtual host class.
+
+L<FS::part_svc> - Service definition class
+
+L<FS::part_svc_column> - Column constraint class
+
+L<FS::export_svc> - Class linking service definitions (see L<FS::part_svc>)
+with exports (see L<FS::part_export>)
+
+L<FS::part_export> - External provisioning export class
+
+L<FS::part_export_option> - Export option class
+
+L<FS::part_pkg> - Package (billing item) definition class
+
+L<FS::pkg_svc> - Class linking package (billing item)
+definitions (see L<FS::part_pkg>) with service definitions
+(see L<FS::part_svc>)
+
+L<FS::agent> - Agent (reseller) class
+
+L<FS::agent_type> - Agent type class
+
+L<FS::type_pkgs> - Class linking agent types (see
+L<FS::agent_type>) with package (billing item) definitions
+(see L<FS::part_pkg>)
+
+L<FS::cust_svc> - Service class
+
+L<FS::cust_pkg> - Package (billing item) class
+
+L<FS::cust_main> - Customer class
+
+L<FS::cust_main_invoice> - Invoice destination
+class
+
+L<FS::cust_bill> - Invoice class
+
+L<FS::cust_bill_pkg> - Invoice line item class
+
+L<FS::cust_bill_pkg_detail> - Invoice line item detail class
+
+L<FS::part_bill_event> - Invoice event definition class
+
+L<FS::cust_bill_event> - Completed invoice event class
+
+L<FS::cust_pay> - Payment class
+
+L<FS::cust_bill_pay> - Payment application class
+
+L<FS::cust_credit> - Credit class
+
+L<FS::cust_refund> - Refund class
+
+L<FS::cust_credit_refund> - Refund application class
+
+L<FS::cust_credit_bill> - Credit invoice application class
+
+L<FS::cust_pay_batch> - Credit card transaction queue class
+
+L<FS::prepay_credit> - Prepaid "calling card" credit class.
+
+L<FS::nas> - Network Access Server class
+
+L<FS::port> - NAS port class
+
+L<FS::session> - User login session class
+
+L<FS::queue> - Job queue
+
+L<FS::queue_arg> - Job arguments
+
+L<FS::queue_depend> - Job dependencies
+
+L<FS::msgcat> - Message catalogs
+
+=head1 Remote API modules
+
+L<FS::SignupClient>
+
+L<FS::SessionClient>
+
+L<FS::MailAdminServer>
+
+=head2 Command-line utilities
+
+L<freeside-adduser>
+
+L<freeside-queued>
+
+L<freeside-daily>
+
+L<freeside-expiration-alerter>
+
+L<freeside-email>
+
+L<freeside-cc-receipts-report>
+
+L<freeside-credit-report>
+
+L<freeside-receivables-report>
+
+L<freeside-tax-report>
+
+L<freeside-bill>
+
+L<freeside-overdue>
+
+=head2 User Interface classes (under (stalled) development; not yet usable)
+
+L<FS::UI::Base> - User-interface base class
+
+L<FS::UI::Gtk> - Gtk user-interface class
+
+L<FS::UI::CGI> - CGI (HTML) user-interface class
+
+L<FS::UI::agent> - agent table user-interface class
+
+=head2 Notes
+
+To quote perl(1), "If you're intending to read these straight through for the
+first time, the suggested order will tend to reduce the number of forward
+references."
+
+If you've never used OO modules before,
+http://www.perl.com/doc/FMTEYEWTK/easy_objects.html might help you out.
+
+=head1 DESCRIPTION
+
+Freeside is a billing and administration package for Internet Service
+Providers.
+
+The Freeside home page is at <http://www.sisd.com/freeside>.
+
+The main documentation is in httemplate/docs.
+
+=head1 SUPPORT
+
+A mailing list for users is available.  Send a blank message to
+<ivan-freeside-subscribe@sisd.com> to subscribe.
+
+A mailing list for developers is available.  It is intended to be lower volume
+and higher SNR than the users list.  Send a blank message to
+<ivan-freeside-devel-subscribe@sisd.com> to subscribe.
+
+Commercial support is available; see
+<http://www.sisd.com/freeside/commercial.html>.
+
+=head1 AUTHOR
+
+Primarily Ivan Kohler <ivan@sisd.com>, with help from many kind folks.
+
+See the CREDITS file in the Freeside distribution for a (hopefully) complete
+list and the individal files for details.
+
+=head1 SEE ALSO
+
+perl(1), main Freeside documentation in htdocs/docs/
+
+=head1 BUGS
+
+Those modules which would be useful separately should be pulled out, 
+renamed appropriately and uploaded to CPAN.  So far: DBIx::DBSchema, Net::SSH
+and Net::SCP...
+
+=cut
+
diff --git a/FS/FS/CGI.pm b/FS/FS/CGI.pm
new file mode 100644 (file)
index 0000000..86d20f6
--- /dev/null
@@ -0,0 +1,350 @@
+package FS::CGI;
+
+use strict;
+use vars qw(@EXPORT_OK @ISA);
+use Exporter;
+use CGI;
+use URI::URL;
+#use CGI::Carp qw(fatalsToBrowser);
+use FS::UID;
+
+@ISA = qw(Exporter);
+@EXPORT_OK = qw(header menubar idiot eidiot popurl table itable ntable
+                small_custview myexit http_header);
+
+=head1 NAME
+
+FS::CGI - Subroutines for the web interface
+
+=head1 SYNOPSIS
+
+  use FS::CGI qw(header menubar idiot eidiot popurl);
+
+  print header( 'Title', '' );
+  print header( 'Title', menubar('item', 'URL', ... ) );
+
+  idiot "error message"; 
+  eidiot "error message";
+
+  $url = popurl; #returns current url
+  $url = popurl(3); #three levels up
+
+=head1 DESCRIPTION
+
+Provides a few common subroutines for the web interface.
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item header TITLE, MENUBAR
+
+Returns an HTML header.
+
+=cut
+
+sub header {
+  my($title,$menubar,$etc)=@_; #$etc is for things like onLoad= etc.
+  #use Carp;
+  $etc = '' unless defined $etc;
+
+  my $x =  <<END;
+    <HTML>
+      <HEAD>
+        <TITLE>
+          $title
+        </TITLE>
+        <META HTTP-Equiv="Cache-Control" Content="no-cache">
+        <META HTTP-Equiv="Pragma" Content="no-cache">
+        <META HTTP-Equiv="Expires" Content="0"> 
+      </HEAD>
+      <BODY BGCOLOR="#e8e8e8"$etc>
+          <FONT SIZE=7>
+            $title
+          </FONT>
+          <BR><BR>
+END
+  $x .=  $menubar. "<BR><BR>" if $menubar;
+  $x;
+}
+
+=item http_header
+
+Sets an http header.
+
+=cut
+
+sub http_header {
+  my ( $header, $value ) = @_;
+  if (exists $ENV{MOD_PERL}) {
+    if ( defined $main::Response
+         && $main::Response->isa('Apache::ASP::Response') ) {  #Apache::ASP
+      if ( $header =~ /^Content-Type$/ ) {
+        $main::Response->{ContentType} = $value;
+      } else {
+        $main::Response->AddHeader( $header => $value );
+      }
+    } elsif ( defined $HTML::Mason::Commands::r  ) { #Mason
+      ## is this the correct pacakge for $r ???  for 1.0x and 1.1x ?
+      if ( $header =~ /^Content-Type$/ ) {
+        $HTML::Mason::Commands::r->content_type($value);
+      } else {
+        $HTML::Mason::Commands::r->header_out( $header => $value );
+      }
+    } else {
+      die "http_header called in unknown environment";
+    }
+  } else {
+    die "http_header called not running under mod_perl";
+  }
+
+}
+
+=item menubar ITEM, URL, ...
+
+Returns an HTML menubar.
+
+=cut
+
+sub menubar { #$menubar=menubar('Main Menu', '../', 'Item', 'url', ... );
+  my($item,$url,@html);
+  while (@_) {
+    ($item,$url)=splice(@_,0,2);
+    push @html, qq!<A HREF="$url">$item</A>!;
+  }
+  join(' | ',@html);
+}
+
+=item idiot ERROR
+
+This is depriciated.  Don't use it.
+
+Sends an HTML error message.
+
+=cut
+
+sub idiot {
+  #warn "idiot depriciated";
+  my($error)=@_;
+#  my $cgi = &FS::UID::cgi();
+#  if ( $cgi->isa('CGI::Base') ) {
+#    no strict 'subs';
+#    &CGI::Base::SendHeaders;
+#  } else {
+#    print $cgi->header( @FS::CGI::header );
+#  }
+  print <<END;
+<HTML>
+  <HEAD>
+    <TITLE>Error processing your request</TITLE>
+    <META HTTP-Equiv="Cache-Control" Content="no-cache">
+    <META HTTP-Equiv="Pragma" Content="no-cache">
+    <META HTTP-Equiv="Expires" Content="0"> 
+  </HEAD>
+  <BODY>
+    <CENTER>
+    <H4>Error processing your request</H4>
+    </CENTER>
+    Your request could not be processed because of the following error:
+    <P><B>$error</B>
+  </BODY>
+</HTML>
+END
+
+}
+
+=item eidiot ERROR
+
+This is depriciated.  Don't use it.
+
+Sends an HTML error message, then exits.
+
+=cut
+
+sub eidiot {
+  warn "eidiot depriciated";
+  $HTML::Mason::Commands::r->send_http_header
+    if defined $HTML::Mason::Commands::r;
+  idiot(@_);
+  &myexit();
+}
+
+=item myexit
+
+You probably shouldn't use this; but if you must:
+
+If running under mod_perl, calles Apache::exit, otherwise, calls exit.
+
+=cut
+
+sub myexit {
+  if (exists $ENV{MOD_PERL}) {
+
+    if ( defined $main::Response
+         && $main::Response->isa('Apache::ASP::Response') ) {  #Apache::ASP
+      $main::Response->End();
+      require Apache;
+      Apache::exit();
+    } elsif ( defined $HTML::Mason::Commands::m  ) { #Mason
+      #$HTML::Mason::Commands::m->flush_buffer();
+      $HTML::Mason::Commands::m->abort();
+      die "shouldn't fall through to here (mason \$m->abort didn't)";
+    } else {
+      #??? well, it is $ENV{MOD_PERL}
+      warn "running under unknown mod_perl environment; trying Apache::exit()";
+      require Apache;
+      Apache::exit();
+    }
+  } else {
+    exit;
+  }
+}
+
+=item popurl LEVEL
+
+Returns current URL with LEVEL levels of path removed from the end (default 0).
+
+=cut
+
+sub popurl {
+  my($up)=@_;
+  my $cgi = &FS::UID::cgi;
+  my $url = new URI::URL ( $cgi->isa('Apache') ? $cgi->uri : $cgi->url );
+  my(@path)=$url->path_components;
+  splice @path, 0-$up;
+  $url->path_components(@path);
+  my $x = $url->as_string;
+  $x .= '/' unless $x =~ /\/$/;
+  $x;
+}
+
+=item table
+
+Returns HTML tag for beginning a table.
+
+=cut
+
+sub table {
+  my $col = shift;
+  if ( $col ) {
+    qq!<TABLE BGCOLOR="$col" BORDER=1 WIDTH="100%" CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">!;
+  } else { 
+    '<TABLE BORDER=1 CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">';
+  }
+}
+
+=item itable
+
+Returns HTML tag for beginning an (invisible) table.
+
+=cut
+
+sub itable {
+  my $col = shift;
+  my $cellspacing = shift || 0;
+  if ( $col ) {
+    qq!<TABLE BGCOLOR="$col" BORDER=0 CELLSPACING=$cellspacing WIDTH="100%">!;
+  } else {
+    qq!<TABLE BORDER=0 CELLSPACING=$cellspacing WIDTH="100%">!;
+  }
+}
+
+=item ntable
+
+This is getting silly.
+
+=cut
+
+sub ntable {
+  my $col = shift;
+  my $cellspacing = shift || 0;
+  if ( $col ) {
+    qq!<TABLE BGCOLOR="$col" BORDER=0 CELLSPACING=$cellspacing>!;
+  } else {
+    '<TABLE BORDER CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">';
+  }
+
+}
+
+=item small_custview CUSTNUM || CUST_MAIN_OBJECT, COUNTRYDEFAULT
+
+Sheesh. I should just switch to Mason.
+
+=cut
+
+sub small_custview {
+  use FS::Record qw(qsearchs);
+  use FS::cust_main;
+
+  my $arg = shift;
+  my $countrydefault = shift || 'US';
+
+  my $cust_main = ref($arg) ? $arg
+                  : qsearchs('cust_main', { 'custnum' => $arg } )
+    or die "unknown custnum $arg";
+
+  my $html = 'Customer #<B>'. $cust_main->custnum. '</B>'.
+    ntable('#e8e8e8'). '<TR><TD>'. ntable("#cccccc",2).
+    '<TR><TD ALIGN="right" VALIGN="top">Billing<BR>Address</TD><TD BGCOLOR="#ffffff">'.
+    $cust_main->getfield('last'). ', '. $cust_main->first. '<BR>';
+
+  $html .= $cust_main->company. '<BR>' if $cust_main->company;
+  $html .= $cust_main->address1. '<BR>';
+  $html .= $cust_main->address2. '<BR>' if $cust_main->address2;
+  $html .= $cust_main->city. ', '. $cust_main->state. '  '. $cust_main->zip. '<BR>';
+  $html .= $cust_main->country. '<BR>'
+    if $cust_main->country && $cust_main->country ne $countrydefault;
+
+  $html .= '</TD></TR></TABLE></TD>';
+
+  if ( defined $cust_main->dbdef_table->column('ship_last') ) {
+
+    my $pre = $cust_main->ship_last ? 'ship_' : '';
+
+    $html .= '<TD>'. ntable("#cccccc",2).
+      '<TR><TD ALIGN="right" VALIGN="top">Service<BR>Address</TD><TD BGCOLOR="#ffffff">'.
+      $cust_main->get("${pre}last"). ', '.
+      $cust_main->get("${pre}first"). '<BR>';
+    $html .= $cust_main->get("${pre}company"). '<BR>'
+      if $cust_main->get("${pre}company");
+    $html .= $cust_main->get("${pre}address1"). '<BR>';
+    $html .= $cust_main->get("${pre}address2"). '<BR>'
+      if $cust_main->get("${pre}address2");
+    $html .= $cust_main->get("${pre}city"). ', '.
+             $cust_main->get("${pre}state"). '  '.
+             $cust_main->get("${pre}ship_zip"). '<BR>';
+    $html .= $cust_main->get("${pre}country"). '<BR>'
+      if $cust_main->get("${pre}country")
+         && $cust_main->get("${pre}country") ne $countrydefault;
+
+    $html .= '</TD></TR></TABLE></TD>';
+  }
+
+  $html .= '</TR></TABLE>';
+
+  $html .= '<BR>Balance: <B>$'. $cust_main->balance. '</B><BR>';
+
+  # last payment might be good here too?
+
+  $html;
+}
+
+=back
+
+=head1 BUGS
+
+Not OO.
+
+Not complete.
+
+small_custview sooooo doesn't belong here.  i should just switch to Mason.
+
+=head1 SEE ALSO
+
+L<CGI>, L<CGI::Base>
+
+=cut
+
+1;
+
+
diff --git a/FS/FS/ClientAPI.pm b/FS/FS/ClientAPI.pm
new file mode 100644 (file)
index 0000000..7cbbdbf
--- /dev/null
@@ -0,0 +1,44 @@
+package FS::ClientAPI;
+
+use strict;
+use vars qw(%handler $domain);
+
+%handler = ();
+
+#find modules
+foreach my $INC ( @INC ) {
+  foreach my $file ( glob("$INC/FS/ClientAPI/*.pm") ) {
+    $file =~ /\/(\w+)\.pm$/ or do {
+      warn "unrecognized ClientAPI file: $file";
+      next
+    };
+    my $mod = $1;
+    #warn "using FS::ClientAPI::$mod";
+    eval "use FS::ClientAPI::$mod;";
+    die "error using FS::ClientAPI::$mod: $@" if $@;
+  }
+}
+
+#(sub for modules)
+sub register_handlers {
+  my $self = shift;
+  my %new_handlers = @_;
+  foreach my $key ( keys %new_handlers ) {
+    warn "WARNING: redefining sub $key" if exists $handler{$key};
+    #warn "registering $key";
+    $handler{$key} = $new_handlers{$key};
+  }
+}
+
+#---
+
+sub dispatch {
+  my ( $self, $name ) = ( shift, shift );
+  my $sub = $handler{$name}
+    or die "unknown FS::ClientAPI sub $name (known: ". join(" ", keys %handler );
+    #or die "unknown FS::ClientAPI sub $name";
+  &{$sub}(@_);
+}
+
+1;
+
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
new file mode 100644 (file)
index 0000000..e12e93b
--- /dev/null
@@ -0,0 +1,257 @@
+package FS::ClientAPI::MyAccount;
+
+use strict;
+use vars qw($cache);
+use Digest::MD5 qw(md5_hex);
+use Date::Format;
+use Business::CreditCard;
+use Cache::SharedMemoryCache; #store in db?
+use FS::CGI qw(small_custview); #doh
+use FS::Conf;
+use FS::Record qw(qsearch qsearchs);
+use FS::svc_acct;
+use FS::svc_domain;
+use FS::cust_main;
+use FS::cust_bill;
+use FS::cust_main_county;
+
+use FS::ClientAPI; #hmm
+FS::ClientAPI->register_handlers(
+  'MyAccount/login'            => \&login,
+  'MyAccount/customer_info'    => \&customer_info,
+  'MyAccount/invoice'          => \&invoice,
+  'MyAccount/cancel'           => \&cancel,
+  'MyAccount/payment_info'     => \&payment_info,
+  'MyAccount/process_payment'  => \&process_payment,
+);
+
+#store in db?
+my $cache = new Cache::SharedMemoryCache();
+
+#false laziness w/FS::ClientAPI::passwd::passwd (needs to handle encrypted pw)
+sub login {
+  my $p = shift;
+
+  my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
+    or return { error => "Domain not found" };
+
+  my $svc_acct =
+    ( length($p->{'password'}) < 13
+      && qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
+                                 'domsvc'    => $svc_domain->svcnum,
+                                 '_password' => $p->{'password'}     } )
+    )
+    || qsearchs( 'svc_acct', { 'username'  => $p->{'username'},
+                               'domsvc'    => $svc_domain->svcnum,
+                               '_password' => $p->{'password'}     } );
+
+  unless ( $svc_acct ) { return { error => 'Incorrect password.' } }
+
+  my $session = {
+    'svcnum' => $svc_acct->svcnum,
+  };
+
+  my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+  if ( $cust_pkg ) {
+    my $cust_main = $cust_pkg->cust_main;
+    $session->{'custnum'} = $cust_main->custnum;
+  }
+
+  my $session_id;
+  do {
+    $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
+  } until ( ! defined $cache->get($session_id) ); #just in case
+
+  $cache->set( $session_id, $session, '1 hour' );
+
+  return { 'error'      => '',
+           'session_id' => $session_id,
+         };
+}
+
+sub customer_info {
+  my $p = shift;
+  my $session = $cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my %return;
+
+  my $custnum = $session->{'custnum'};
+
+  if ( $custnum ) { #customer record
+
+    my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+      or return { 'error' => "unknown custnum $custnum" };
+
+    $return{balance} = $cust_main->balance;
+
+    my @open = map {
+                     {
+                       invnum => $_->invnum,
+                       date   => time2str("%b %o, %Y", $_->_date),
+                       owed   => $_->owed,
+                     };
+                   } $cust_main->open_cust_bill;
+    $return{open_invoices} = \@open;
+
+    my $conf = new FS::Conf;
+    $return{small_custview} =
+      small_custview( $cust_main, $conf->config('defaultcountry') );
+
+    $return{name} = $cust_main->first. ' '. $cust_main->get('last');
+
+  } else { #no customer record
+
+    my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
+      or die "unknown svcnum";
+    $return{name} = $svc_acct->email;
+
+  }
+
+  return { 'error'          => '',
+           'custnum'        => $custnum,
+           %return,
+         };
+
+}
+
+sub payment_info {
+  my $p = shift;
+  my $session = $cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my %return;
+
+  my $custnum = $session->{'custnum'};
+
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+    or return { 'error' => "unknown custnum $custnum" };
+
+  $return{balance} = $cust_main->balance;
+
+  $return{payname} = $cust_main->payname
+                     || ( $cust_main->first. ' '. $cust_main->get('last') );
+
+  $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip);
+
+  $return{payby} = $cust_main->payby;
+
+  if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
+    warn $return{card_type} = cardtype($cust_main->payinfo);
+    $return{payinfo} = $cust_main->payinfo;
+
+    if ( $cust_main->paydate  =~ /^(\d{4})-(\d{2})-\d{2}$/ ) { #Pg date format
+      @return{'month', 'year'} = ( $2, $1 );
+    } elsif ( $cust_main->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+      @return{'month', 'year'} = ( $1, $3 );
+    }
+
+  }
+
+  #list all counties/states/countries
+  $return{'cust_main_county'} = 
+      [ map { $_->hashref } qsearch('cust_main_county', {}) ],
+
+  #shortcut for one-country folks
+  my $conf = new FS::Conf;
+  my %states = map { $_->state => 1 }
+                 qsearch('cust_main_county', {
+                   'country' => $conf->config('defaultcountry') || 'US'
+                 } );
+  $return{'states'} = [ sort { $a cmp $b } keys %states ];
+
+  $return{card_types} = {
+    'VISA' => 'VISA card',
+    'MasterCard' => 'MasterCard',
+    'Discover' => 'Discover card',
+    'American Express' => 'American Express card',
+  };
+
+  my $_date = time;
+  $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
+
+  return { 'error' => '',
+           %return,
+         };
+
+};
+
+sub process_payment {
+  my $p = shift;
+
+  my $session = $cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my %return;
+
+  my $custnum = $session->{'custnum'};
+
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+    or return { 'error' => "unknown custnum $custnum" };
+
+  if ( $p->{'save'} ) {
+    my $new = new FS::cust_main { $cust_main->hash };
+    $new->set( $_ => $p->{$_} )
+      foreach qw( payname address1 address2 city state zip payinfo );
+    $new->set( 'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01' );
+    $new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
+    my $error = $new->replace($cust_main);
+    return { 'error' => $error } if $error;
+    $cust_main = $new;
+  }
+
+  my $error = $cust_main->realtime_bop( 'CC', $p->{'amount'}, quiet=>1,
+    'paydate' => $p->{'year'}. '-'. $p->{'month'}. '-01',
+    map { $_ => $p->{$_} }
+      qw( payname address1 address2 city state zip payinfo )
+  );
+  return { 'error' => $error } if $error;
+
+  $cust_main->apply_payments;
+
+  return { 'error' => '' };
+
+}
+
+sub invoice {
+  my $p = shift;
+  my $session = $cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $custnum = $session->{'custnum'};
+
+  my $invnum = $p->{'invnum'};
+
+  my $cust_bill = qsearchs('cust_bill', { 'invnum'  => $invnum,
+                                          'custnum' => $custnum } )
+    or return { 'error' => "Can't find invnum" };
+
+  #my %return;
+
+  return { 'error'        => '',
+           'invnum'       => $invnum,
+           'invoice_text' => join('', $cust_bill->print_text ),
+         };
+
+}
+
+sub cancel {
+  my $p = shift;
+  my $session = $cache->get($p->{'session_id'})
+    or return { 'error' => "Can't resume session" }; #better error message
+
+  my $custnum = $session->{'custnum'};
+
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+    or return { 'error' => "unknown custnum $custnum" };
+
+  my @errors = $cust_main->cancel;
+
+  my $error = scalar(@errors) ? join(' / ', @errors) : '';
+
+  return { 'error' => $error };
+
+}
+
+1;
+
diff --git a/FS/FS/ClientAPI/passwd.pm b/FS/FS/ClientAPI/passwd.pm
new file mode 100644 (file)
index 0000000..016ebff
--- /dev/null
@@ -0,0 +1,57 @@
+package FS::ClientAPI::passwd;
+
+use strict;
+use FS::Record qw(qsearchs);
+use FS::svc_acct;
+#use FS::svc_domain;
+
+use FS::ClientAPI; #hmm
+FS::ClientAPI->register_handlers(
+  'passwd/passwd' => \&passwd,
+  'passwd/chfn' => \&chfn,
+  'passwd/chsh' => \&chsh,
+);
+
+sub passwd {
+  my $packet = shift;
+
+  my $domain = $FS::ClientAPI::domain || $packet->{'domain'};
+  my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } )
+    or return { error => "Domain $domain not found" };
+
+  my $old_password = $packet->{'old_password'};
+  my $new_password = $packet->{'new_password'};
+  my $new_gecos = $packet->{'new_gecos'};
+  my $new_shell = $packet->{'new_shell'};
+
+#false laziness w/FS::ClientAPI::MyAccount::login (needs to handle encrypted pw)
+  my $svc_acct =
+    ( length($old_password) < 13
+      && qsearchs( 'svc_acct', { 'username'  => $packet->{'username'},
+                                 'domsvc'    => $svc_domain->svcnum,
+                                 '_password' => $old_password } )
+    )
+    || qsearchs( 'svc_acct', { 'username'  => $packet->{'username'},
+                               'domsvc'    => $svc_domain->svcnum,
+                               '_password' => $old_password } );
+
+  unless ( $svc_acct ) { return { error => 'Incorrect password.' } }
+
+  my %hash = $svc_acct->hash;
+  my $new_svc_acct = new FS::svc_acct ( \%hash );
+  $new_svc_acct->setfield('_password', $new_password ) 
+    if $new_password && $new_password ne $old_password;
+  $new_svc_acct->setfield('finger',$new_gecos) if $new_gecos;
+  $new_svc_acct->setfield('shell',$new_shell) if $new_shell;
+  my $error = $new_svc_acct->replace($svc_acct);
+
+  return { error => $error };
+
+}
+
+sub chfn {}
+
+sub chsh {}
+
+1;
+
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
new file mode 100644 (file)
index 0000000..706ebe7
--- /dev/null
@@ -0,0 +1,1059 @@
+package FS::Conf;
+
+use vars qw($default_dir @config_items $DEBUG );
+use IO::File;
+use File::Basename;
+use FS::ConfItem;
+
+$DEBUG = 0;
+
+=head1 NAME
+
+FS::Conf - Freeside configuration values
+
+=head1 SYNOPSIS
+
+  use FS::Conf;
+
+  $conf = new FS::Conf "/config/directory";
+
+  $FS::Conf::default_dir = "/config/directory";
+  $conf = new FS::Conf;
+
+  $dir = $conf->dir;
+
+  $value = $conf->config('key');
+  @list  = $conf->config('key');
+  $bool  = $conf->exists('key');
+
+  $conf->touch('key');
+  $conf->set('key' => 'value');
+  $conf->delete('key');
+
+  @config_items = $conf->config_items;
+
+=head1 DESCRIPTION
+
+Read and write Freeside configuration values.  Keys currently map to filenames,
+but this may change in the future.
+
+=head1 METHODS
+
+=over 4
+
+=item new [ DIRECTORY ]
+
+Create a new configuration object.  A directory arguement is required if
+$FS::Conf::default_dir has not been set.
+
+=cut
+
+sub new {
+  my($proto,$dir) = @_;
+  my($class) = ref($proto) || $proto;
+  my($self) = { 'dir' => $dir || $default_dir } ;
+  bless ($self, $class);
+}
+
+=item dir
+
+Returns the directory.
+
+=cut
+
+sub dir {
+  my($self) = @_;
+  my $dir = $self->{dir};
+  -e $dir or die "FATAL: $dir doesn't exist!";
+  -d $dir or die "FATAL: $dir isn't a directory!";
+  -r $dir or die "FATAL: Can't read $dir!";
+  -x $dir or die "FATAL: $dir not searchable (executable)!";
+  $dir =~ /^(.*)$/;
+  $1;
+}
+
+=item config KEY
+
+Returns the configuration value or values (depending on context) for key.
+
+=cut
+
+sub config {
+  my($self,$file)=@_;
+  my($dir)=$self->dir;
+  my $fh = new IO::File "<$dir/$file" or return;
+  if ( wantarray ) {
+    map {
+      /^(.*)$/
+        or die "Illegal line (array context) in $dir/$file:\n$_\n";
+      $1;
+    } <$fh>;
+  } else {
+    <$fh> =~ /^(.*)$/
+      or die "Illegal line (scalar context) in $dir/$file:\n$_\n";
+    $1;
+  }
+}
+
+=item exists KEY
+
+Returns true if the specified key exists, even if the corresponding value
+is undefined.
+
+=cut
+
+sub exists {
+  my($self,$file)=@_;
+  my($dir) = $self->dir;
+  -e "$dir/$file";
+}
+
+=item touch KEY
+
+Creates the specified configuration key if it does not exist.
+
+=cut
+
+sub touch {
+  my($self, $file) = @_;
+  my $dir = $self->dir;
+  unless ( $self->exists($file) ) {
+    warn "[FS::Conf] TOUCH $file\n" if $DEBUG;
+    system('touch', "$dir/$file");
+  }
+}
+
+=item set KEY VALUE
+
+Sets the specified configuration key to the given value.
+
+=cut
+
+sub set {
+  my($self, $file, $value) = @_;
+  my $dir = $self->dir;
+  $value =~ /^(.*)$/s;
+  $value = $1;
+  unless ( join("\n", @{[ $self->config($file) ]}) eq $value ) {
+    warn "[FS::Conf] SET $file\n" if $DEBUG;
+#    warn "$dir" if is_tainted($dir);
+#    warn "$dir" if is_tainted($file);
+    chmod 0644, "$dir/$file";
+    my $fh = new IO::File ">$dir/$file" or return;
+    chmod 0644, "$dir/$file";
+    print $fh "$value\n";
+  }
+}
+#sub is_tainted {
+#             return ! eval { join('',@_), kill 0; 1; };
+#         }
+
+=item delete KEY
+
+Deletes the specified configuration key.
+
+=cut
+
+sub delete {
+  my($self, $file) = @_;
+  my $dir = $self->dir;
+  if ( $self->exists($file) ) {
+    warn "[FS::Conf] DELETE $file\n";
+    unlink "$dir/$file";
+  }
+}
+
+=item config_items
+
+Returns all of the possible configuration items as FS::ConfItem objects.  See
+L<FS::ConfItem>.
+
+=cut
+
+sub config_items {
+  my $self = shift; 
+  #quelle kludge
+  @config_items,
+  map { 
+        my $basename = basename($_);
+        $basename =~ /^(.*)$/;
+        $basename = $1;
+        new FS::ConfItem {
+                           'key'         => $basename,
+                           'section'     => 'billing',
+                           'description' => 'Alternate template file for invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
+                           'type'        => 'textarea',
+                         }
+      } glob($self->dir. '/invoice_template_*')
+  ;
+}
+
+=back
+
+=head1 BUGS
+
+If this was more than just crud that will never be useful outside Freeside I'd
+worry that config_items is freeside-specific and icky.
+
+=head1 SEE ALSO
+
+"Configuration" in the web interface (config/config.cgi).
+
+httemplate/docs/config.html
+
+=cut
+
+@config_items = map { new FS::ConfItem $_ } (
+
+  {
+    'key'         => 'address',
+    'section'     => 'deprecated',
+    'description' => 'This configuration option is no longer used.  See <a href="#invoice_template">invoice_template</a> instead.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'alerter_template',
+    'section'     => 'billing',
+    'description' => 'Template file for billing method expiration alerts.  See the <a href="../docs/billing.html#invoice_template">billing documentation</a> for details.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'apacheroot',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>www_shellcommands</i> <a href="../browse/part_export.cgi">export</a> instead.  The directory containing Apache virtual hosts',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'apacheip',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add an <i>apache</i> <a href="../browse/part_export.cgi">export</a> instead.  Used to be the current IP address to assign to new virtual hosts',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'apachemachine',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>www_shellcommands</i> <a href="../browse/part_export.cgi">export</a> instead.  A machine with the apacheroot directory and user home directories.  The existance of this file enables setup of virtual host directories, and, in conjunction with the `home\' configuration file, symlinks into user home directories.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'apachemachines',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add an <i>apache</i> <a href="../browse/part_export.cgi">export</a> instead.  Used to be Apache machines, one per line.  This enables export of `/etc/apache/vhosts.conf\', which can be included in your Apache configuration via the <a href="http://www.apache.org/docs/mod/core.html#include">Include</a> directive.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'bindprimary',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>bind</i> <a href="../browse/part_export.cgi">export</a> instead.  Your BIND primary nameserver.  This enables export of /var/named/named.conf and zone files into /var/named',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'bindsecondaries',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>bind_slave</i> <a href="../browse/part_export.cgi">export</a> instead.  Your BIND secondary nameservers, one per line.  This enables export of /var/named/named.conf',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'business-onlinepayment',
+    'section'     => 'billing',
+    'description' => '<a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> support, at least three lines: processor, login, and password.  An optional fourth line specifies the action or actions (multiple actions are separated with `,\': for example: `Authorization Only, Post Authorization\').    Optional additional lines are passed to Business::OnlinePayment as %processor_options.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'business-onlinepayment-ach',
+    'section'     => 'billing',
+    'description' => 'Alternate <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> support for ACH transactions (defaults to regular <b>business-onlinepayment</b>).  At least three lines: processor, login, and password.  An optional fourth line specifies the action or actions (multiple actions are separated with `,\': for example: `Authorization Only, Post Authorization\').    Optional additional lines are passed to Business::OnlinePayment as %processor_options.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'business-onlinepayment-description',
+    'section'     => 'billing',
+    'description' => 'String passed as the description field to <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a>.  Evaluated as a double-quoted perl string, with the following variables available: <code>$agent</code> (the agent name), and <code>$pkgs</code> (a comma-separated list of packages to which the invoiced being charged applies)',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'bsdshellmachines',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>bsdshell</i> <a href="../browse/part_export.cgi">export</a> instead.  Your BSD flavored shell (and mail) machines, one per line.  This enables export of `/etc/passwd\' and `/etc/master.passwd\'.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'countrydefault',
+    'section'     => 'UI',
+    'description' => 'Default two-letter country code (if not supplied, the default is `US\')',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'cyrus',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>cyrus</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to integrate with <a href="http://asg.web.cmu.edu/cyrus/imapd/">Cyrus IMAP Server</a>, three lines: IMAP server, admin username, and admin password.  Cyrus::IMAP::Admin should be installed locally and the connection to the server secured.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'cp_app',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>cp</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to integrate with <a href="http://www.cp.net/">Critial Path Account Provisioning Protocol</a>, four lines: "host:port", username, password, and workgroup (for new users).',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'deletecustomers',
+    'section'     => 'UI',
+    'description' => 'Enable customer deletions.  Be very careful!  Deleting a customer will remove all traces that this customer ever existed!  It should probably only be used when auditing a legacy database.  Normally, you cancel all of a customers\' packages if they cancel service.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'deletepayments',
+    'section'     => 'UI',
+    'description' => 'Enable deletion of unclosed payments.  Be very careful!  Only delete payments that were data-entry errors, not adjustments. Optionally specify one or more comma-separated email addresses to be notified when a payment is deleted.',
+    'type'        => [qw( checkbox text )],
+  },
+
+  {
+    'key'         => 'unapplypayments',
+    'section'     => 'UI',
+    'description' => 'Enable "unapplication" of unclosed payments.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'dirhash',
+    'section'     => 'shell',
+    'description' => 'Optional numeric value to control directory hashing.  If positive, hashes directories for the specified number of levels from the front of the username.  If negative, hashes directories for the specified number of levels from the end of the username.  Some examples: <ul><li>1: user -> <a href="#home">/home</a>/u/user<li>2: user -> <a href="#home">/home</a>/u/s/user<li>-1: user -> <a href="#home">/home</a>/r/user<li>-2: user -> <a href="#home">home</a>/r/e/user</ul>',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'disable_customer_referrals',
+    'section'     => 'UI',
+    'description' => 'Disable new customer-to-customer referrals in the web interface',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'editreferrals',
+    'section'     => 'UI',
+    'description' => 'Enable advertising source modification for existing customers',
+    'type'       => 'checkbox',
+  },
+
+  {
+    'key'         => 'emailinvoiceonly',
+    'section'     => 'billing',
+    'description' => 'Disables postal mail invoices',
+    'type'       => 'checkbox',
+  },
+
+  {
+    'key'         => 'disablepostalinvoicedefault',
+    'section'     => 'billing',
+    'description' => 'Disables postal mail invoices as the default option in the UI.  Be careful not to setup customers which are not sent invoices.  See <a href ="#emailinvoiceauto">emailinvoiceauto</a>.',
+    'type'       => 'checkbox',
+  },
+
+  {
+    'key'         => 'emailinvoiceauto',
+    'section'     => 'billing',
+    'description' => 'Automatically adds new accounts to the email invoice list',
+    'type'       => 'checkbox',
+  },
+
+  {
+    'key'         => 'exclude_ip_addr',
+    'section'     => '',
+    'description' => 'Exclude these from the list of available broadband service IP addresses. (One per line)',
+    'type'        => 'textarea',
+  },
+  
+  {
+    'key'         => 'erpcdmachines',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, ERPCD is no longer supported.  Used to be ERPCD authenticaion machines, one per line.  This enables export of `/usr/annex/acp_passwd\' and `/usr/annex/acp_dialup\'',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'hidecancelledpackages',
+    'section'     => 'UI',
+    'description' => 'Prevent cancelled packages from showing up in listings (though they will still be in the database)',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'hidecancelledcustomers',
+    'section'     => 'UI',
+    'description' => 'Prevent customers with only cancelled packages from showing up in listings (though they will still be in the database)',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'home',
+    'section'     => 'required',
+    'description' => 'For new users, prefixed to username to create a directory name.  Should have a leading but not a trailing slash.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'icradiusmachines',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to enable radcheck and radreply table population - by default in the Freeside database, or in the database specified by the <a href="http://rootwood.haze.st/aspside/config/config-view.cgi#icradius_secrets">icradius_secrets</a> config option (the radcheck and radreply tables needs to be created manually).  You do not need to use MySQL for your Freeside database to export to an ICRADIUS/FreeRADIUS MySQL database with this option.  <blockquote><b>ADDITIONAL DEPRECATED FUNCTIONALITY</b> (instead use <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">MySQL replication</a> or point icradius_secrets to the external database) - your <a href="ftp://ftp.cheapnet.net/pub/icradius">ICRADIUS</a> machines or <a href="http://www.freeradius.org/">FreeRADIUS</a> (with MySQL authentication) machines, one per line.  Machines listed in this file will have the radcheck table exported to them.  Each line should contain four items, separted by whitespace: machine name, MySQL database name, MySQL username, and MySQL password.  For example: <CODE>"radius.isp.tld&nbsp;radius_db&nbsp;radius_user&nbsp;passw0rd"</CODE></blockquote>',
+    'type'        => [qw( checkbox textarea )],
+  },
+
+  {
+    'key'         => 'icradius_mysqldest',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead.  Used to be the destination directory for the MySQL databases, on the ICRADIUS/FreeRADIUS machines.  Defaults to "/usr/local/var/".',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'icradius_mysqlsource',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead.  Used to be the source directory for for the MySQL radcheck table files, on the Freeside machine.  Defaults to "/usr/local/var/freeside".',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'icradius_secrets',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to specify a database for ICRADIUS/FreeRADIUS export.  Three lines: DBI data source, username and password.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'invoice_from',
+    'section'     => 'required',
+    'description' => 'Return address on email invoices',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'invoice_template',
+    'section'     => 'required',
+    'description' => 'Required template file for invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'lpr',
+    'section'     => 'required',
+    'description' => 'Print command for paper invoices, for example `lpr -h\'',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'maildisablecatchall',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, now the default.  Turning this option on used to disable the requirement that each virtual domain have a catch-all mailbox.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'money_char',
+    'section'     => '',
+    'description' => 'Currency symbol - defaults to `$\'',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'mxmachines',
+    'section'     => 'deprecated',
+    'description' => 'MX entries for new domains, weight and machine, one per line, with trailing `.\'',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'nsmachines',
+    'section'     => 'deprecated',
+    'description' => 'NS nameservers for new domains, one per line, with trailing `.\'',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'defaultrecords',
+    'section'     => 'BIND',
+    'description' => 'DNS entries to add automatically when creating a domain',
+    'type'        => 'editlist',
+    'editlist_parts' => [ { type=>'text' },
+                          { type=>'immutable', value=>'IN' },
+                          { type=>'select',
+                            select_enum=>{ map { $_=>$_ } qw(A CNAME MX NS)} },
+                          { type=> 'text' }, ],
+  },
+
+  {
+    'key'         => 'arecords',
+    'section'     => 'deprecated',
+    'description' => 'A list of tab seperated CNAME records to add automatically when creating a domain',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'cnamerecords',
+    'section'     => 'deprecated',
+    'description' => 'A list of tab seperated CNAME records to add automatically when creating a domain',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'nismachines',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>.  Your NIS master (not slave master) machines, one per line.  This enables export of `/etc/global/passwd\' and `/etc/global/shadow\'.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'passwordmin',
+    'section'     => 'password',
+    'description' => 'Minimum password length (default 6)',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'passwordmax',
+    'section'     => 'password',
+    'description' => 'Maximum password length (default 8) (don\'t set this over 12 if you need to import or export crypt() passwords)',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'qmailmachines',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add <i>qmail</i> and <i>shellcommands</i> <a href="../browse/part_export.cgi">exports</a> instead.  This option used to export `/var/qmail/control/virtualdomains\', `/var/qmail/control/recipientmap\', and `/var/qmail/control/rcpthosts\'.  Setting this option (even if empty) also turns on user `.qmail-extension\' file maintenance in conjunction with the <b>shellmachine</b> option.',
+    'type'        => [qw( checkbox textarea )],
+  },
+
+  {
+    'key'         => 'radiusmachines',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add an <i>sqlradius</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to export to be: your RADIUS authentication machines, one per line.  This enables export of `/etc/raddb/users\'.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'referraldefault',
+    'section'     => 'UI',
+    'description' => 'Default referral, specified by refnum',
+    'type'        => 'text',
+  },
+
+#  {
+#    'key'         => 'registries',
+#    'section'     => 'required',
+#    'description' => 'Directory which contains domain registry information.  Each registry is a directory.',
+#  },
+
+  {
+    'key'         => 'report_template',
+    'section'     => 'required',
+    'description' => 'Required template file for reports.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
+    'type'        => 'textarea',
+  },
+
+
+  {
+    'key'         => 'maxsearchrecordsperpage',
+    'section'     => 'UI',
+    'description' => 'If set, number of search records to return per page.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'sendmailconfigpath',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>sendmail</i> <a href="../browse/part_export.cgi">export</a> instead.  Used to be sendmail configuration file path.  Defaults to `/etc\'.  Many newer distributions use `/etc/mail\'.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'sendmailmachines',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>sendmail</i> <a href="../browse/part_export.cgi">export</a> instead.  Used to be sendmail machines, one per line.  This enables export of `/etc/virtusertable\' and `/etc/sendmail.cw\'.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'sendmailrestart',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>sendmail</i> <a href="../browse/part_export.cgi">export</a> instead.  Used to define the command which is run on sendmail machines after files are copied.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'session-start',
+    'section'     => 'session',
+    'description' => 'If defined, the command which is executed on the Freeside machine when a session begins.  The contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$ip</code>, <code>$nasip</code> and <code>$nasfqdn</code>, which are the IP address of the starting session, and the IP address and fully-qualified domain name of the NAS this session is on.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'session-stop',
+    'section'     => 'session',
+    'description' => 'If defined, the command which is executed on the Freeside machine when a session ends.  The contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$ip</code>, <code>$nasip</code> and <code>$nasfqdn</code>, which are the IP address of the starting session, and the IP address and fully-qualified domain name of the NAS this session is on.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'shellmachine',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>shellcommands</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to contain a single machine with user home directories mounted.  This enables home directory creation, renaming and archiving/deletion.  In conjunction with `qmailmachines\', it also enables `.qmail-extension\' file maintenance.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'shellmachine-useradd',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>shellcommands</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to contain command(s) to run on shellmachine when an account is created.  If the <b>shellmachine</b> option is set but this option is not, <code>useradd -d $dir -m -s $shell -u $uid $username</code> is the default.  If this option is set but empty, <code>cp -pr /etc/skel $dir; chown -R $uid.$gid $dir</code> is the default instead.  Otherwise the value is evaluated as a double-quoted perl string, with the following variables available: <code>$username</code>, <code>$uid</code>, <code>$gid</code>, <code>$dir</code>, and <code>$shell</code>.',
+    'type'        => [qw( checkbox text )],
+  },
+
+  {
+    'key'         => 'shellmachine-userdel',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>shellcommands</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to contain command(s) to run on shellmachine when an account is deleted.  If the <b>shellmachine</b> option is set but this option is not, <code>userdel $username</code> is the default.  If this option is set but empty, <code>rm -rf $dir</code> is the default instead.  Otherwise the value is evaluated as a double-quoted perl string, with the following variables available: <code>$username</code> and <code>$dir</code>.',
+    'type'        => [qw( checkbox text )],
+  },
+
+  {
+    'key'         => 'shellmachine-usermod',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>shellcommands</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to contain command(s) to run on shellmachine when an account is modified.  If the <b>shellmachine</b> option is set but this option is empty, <code>[ -d $old_dir ] &amp;&amp; mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $uid.$gid $new_dir; rm -rf $old_dir )</code> is the default.  Otherwise the contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$old_dir</code>, <code>$new_dir</code>, <code>$uid</code> and <code>$gid</code>.',
+    #'type'        => [qw( checkbox text )],
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'shellmachines',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>sysvshell</i> <a href="../browse/part_export.cgi">export</a> instead.  Your Linux and System V flavored shell (and mail) machines, one per line.  This enables export of `/etc/passwd\' and `/etc/shadow\' files.',
+     'type'        => 'textarea',
+ },
+
+  {
+    'key'         => 'shells',
+    'section'     => 'required',
+    'description' => 'Legal shells (think /etc/shells).  You probably want to `cut -d: -f7 /etc/passwd | sort | uniq\' initially so that importing doesn\'t fail with `Illegal shell\' errors, then remove any special entries afterwords.  A blank line specifies that an empty shell is permitted.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'showpasswords',
+    'section'     => 'UI',
+    'description' => 'Display unencrypted user passwords in the web interface',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'signupurl',
+    'section'     => 'UI',
+    'description' => 'if you are using customer-to-customer referrals, and you enter the URL of your <a href="../docs/signup.html">signup server CGI</a>, the customer view screen will display a customized link to the signup server with the appropriate customer as referral',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'smtpmachine',
+    'section'     => 'required',
+    'description' => 'SMTP relay for Freeside\'s outgoing mail',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'soadefaultttl',
+    'section'     => 'BIND',
+    'description' => 'SOA default TTL for new domains.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'soaemail',
+    'section'     => 'BIND',
+    'description' => 'SOA email for new domains, in BIND form (`.\' instead of `@\'), with trailing `.\'',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'soaexpire',
+    'section'     => 'BIND',
+    'description' => 'SOA expire for new domains',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'soamachine',
+    'section'     => 'BIND',
+    'description' => 'SOA machine for new domains, with trailing `.\'',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'soarefresh',
+    'section'     => 'BIND',
+    'description' => 'SOA refresh for new domains',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'soaretry',
+    'section'     => 'BIND',
+    'description' => 'SOA retry for new domains',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'statedefault',
+    'section'     => 'UI',
+    'description' => 'Default state or province (if not supplied, the default is `CA\')',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'radiusprepend',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, real-time text radius now edits an existing file in place - just (turn off freeside-queued and) edit your RADIUS users file directly.  The contents used to be be prepended to the top of the RADIUS users file (text exports only).',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'textradiusprepend',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, use RADIUS check attributes instead.  The contents used to be prepended to the first line of a user\'s RADIUS entry in text exports.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'unsuspendauto',
+    'section'     => 'billing',
+    'description' => 'Enables the automatic unsuspension of suspended packages when a customer\'s balance due changes from positive to zero or negative as the result of a payment or credit',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'usernamemin',
+    'section'     => 'username',
+    'description' => 'Minimum username length (default 2)',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'usernamemax',
+    'section'     => 'username',
+    'description' => 'Maximum username length',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'username-ampersand',
+    'section'     => 'username',
+    'description' => 'Allow the ampersand character (&amp;) in usernames.  Be careful when using this option in conjunction with <a href="#shellmachine-useradd">shellmachine-useradd</a> and other configuration options which execute shell commands, as the ampersand will be interpreted by the shell if not quoted.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'username-letter',
+    'section'     => 'username',
+    'description' => 'Usernames must contain at least one letter',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'username-letterfirst',
+    'section'     => 'username',
+    'description' => 'Usernames must start with a letter',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'username-noperiod',
+    'section'     => 'username',
+    'description' => 'Disallow periods in usernames',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'username-nounderscore',
+    'section'     => 'username',
+    'description' => 'Disallow underscores in usernames',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'username-nodash',
+    'section'     => 'username',
+    'description' => 'Disallow dashes in usernames',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'username-uppercase',
+    'section'     => 'username',
+    'description' => 'Allow uppercase characters in usernames',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'username_policy',
+    'section'     => 'deprecated',
+    'description' => 'This file controls the mechanism for preventing duplicate usernames in passwd/radius files exported from svc_accts.  This should be one of \'prepend domsvc\' \'append domsvc\' \'append domain\' or \'append @domain\'',
+    'type'        => 'select',
+    'select_enum' => [ 'prepend domsvc', 'append domsvc', 'append domain', 'append @domain' ],
+    #'type'        => 'text',
+  },
+
+  {
+    'key'         => 'vpopmailmachines',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>vpopmail</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to contain your vpopmail pop toasters, one per line.  Each line is of the form "machinename vpopdir vpopuid vpopgid".  For example: <code>poptoaster.domain.tld /home/vpopmail 508 508</code>  Note: vpopuid and vpopgid are values taken from the vpopmail machine\'s /etc/passwd',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'vpopmailrestart',
+    'section'     => 'deprecated',
+    'description' => '<b>DEPRECATED</b>, add a <i>vpopmail</i> <a href="../browse/part_export.cgi">export</a> instead.  This option used to define the shell commands to run on vpopmail machines after files are copied.  An example can be found in eg/vpopmailrestart of the source distribution.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'safe-part_pkg',
+    'section'     => 'UI',
+    'description' => 'Validates package definition setup and recur expressions against a preset list.  Useful for webdemos, annoying to powerusers.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'safe-part_bill_event',
+    'section'     => 'UI',
+    'description' => 'Validates invoice event expressions against a preset list.  Useful for webdemos, annoying to powerusers.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'show_ss',
+    'section'     => 'UI',
+    'description' => 'Turns on display/collection of SS# in the web interface.',
+    'type'        => 'checkbox',
+  },
+
+  { 
+    'key'         => 'agent_defaultpkg',
+    'section'     => 'UI',
+    'description' => 'Setting this option will cause new packages to be available to all agent types by default.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'legacy_link',
+    'section'     => 'UI',
+    'description' => 'Display options in the web interface to link legacy pre-Freeside services.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'queue_dangerous_controls',
+    'section'     => 'UI',
+    'description' => 'Enable queue modification controls on account pages and for new jobs.  Unless you are a developer working on new export code, you should probably leave this off to avoid causing provisioning problems.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'security_phrase',
+    'section'     => 'password',
+    'description' => 'Enable the tracking of a "security phrase" with each account.  Not recommended, as it is vulnerable to social engineering.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'locale',
+    'section'     => 'UI',
+    'description' => 'Message locale',
+    'type'        => 'select',
+    'select_enum' => [ qw(en_US) ],
+  },
+
+  {
+    'key'         => 'selfservice_server-quiet',
+    'section'     => '',
+    'description' => 'Disable decline and cancel emails generated by transactions initiated by the selfservice server. Not recommended, unless the customer will get instant feedback from a customer service UI, and receiving an email would be confusing/overkill.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'signup_server-quiet',
+    'section'     => '',
+    'description' => 'Disable decline and cancel emails generated by transactions initiated by the signup server. Not recommended, unless the customer will get instant feedback from a customer service UI, and receiving an email would be confusing/overkill. Does not disable welcome emails.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'signup_server-payby',
+    'section'     => '',
+    'description' => 'Acceptable payment types for the signup server',
+    'type'        => 'selectmultiple',
+    'select_enum' => [ qw(CARD DCRD CHEK DCHK LECB PREPAY BILL COMP) ],
+  },
+
+  {
+    'key'         => 'signup_server-email',
+    'section'     => '',
+    'description' => 'Comma-separated list of email addresses to receive notification of signups via the signup server.',
+    'type'        => 'text',
+  },
+
+
+  {
+    'key'         => 'show-msgcat-codes',
+    'section'     => 'UI',
+    'description' => 'Show msgcat codes in error messages.  Turn this option on before reporting errors to the mailing list.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'signup_server-realtime',
+    'section'     => '',
+    'description' => 'Run billing for signup server signups immediately, and suspend accounts which subsequently have a balance.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'declinetemplate',
+    'section'     => 'billing',
+    'description' => 'Template file for credit card decline emails.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'emaildecline',
+    'section'     => 'billing',
+    'description' => 'Enable emailing of credit card decline notices.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'cancelmessage',
+    'section'     => 'billing',
+    'description' => 'Template file for cancellation emails.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'cancelsubject',
+    'section'     => 'billing',
+    'description' => 'Subject line for cancellation emails.',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'emailcancel',
+    'section'     => 'billing',
+    'description' => 'Enable emailing of cancellation notices.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'require_cardname',
+    'section'     => 'billing',
+    'description' => 'Require an "Exact name on card" to be entered explicitly; don\'t default to using the first and last name.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'enable_taxclasses',
+    'section'     => 'billing',
+    'description' => 'Enable per-package tax classes',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'welcome_email',
+    'section'     => '',
+    'description' => 'Template file for welcome email.  Welcome emails are sent to the customer email invoice destination(s) each time a svc_acct record is created.  See the <a href="http://search.cpan.org/doc/MJD/Text-Template-1.42/Template.pm">Text::Template</a> documentation for details on the template substitution language.  The following variables are available: <code>$username</code>, <code>$password</code>, <code>$first</code>, <code>$last</code> and <code>$pkg</code>.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'welcome_email-from',
+    'section'     => '',
+    'description' => 'From: address header for welcome email',
+    'type'        => 'text',
+  },
+
+  {
+    'key'         => 'welcome_email-subject',
+    'section'     => '',
+    'description' => 'Subject: header for welcome email',
+    'type'        => 'text',
+  },
+  
+  {
+    'key'         => 'welcome_email-mimetype',
+    'section'     => '',
+    'description' => 'MIME type for welcome email',
+    'type'        => 'select',
+    'select_enum' => [ 'text/plain', 'text/html' ],
+  },
+
+  {
+    'key'         => 'payby-default',
+    'section'     => 'UI',
+    'description' => 'Default payment type.  HIDE disables display of billing information and sets customers to BILL.',
+    'type'        => 'select',
+    'select_enum' => [ '', qw(CARD DCRD CHEK DCHK LECB BILL COMP HIDE) ],
+  },
+
+  {
+    'key'         => 'svc_acct-notes',
+    'section'     => 'UI',
+    'description' => 'Extra HTML to be displayed on the Account View screen.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'radius-password',
+    'section'     => '',
+    'description' => 'RADIUS attribute for plain-text passwords.',
+    'type'        => 'select',
+    'select_enum' => [ 'Password', 'User-Password' ],
+  },
+
+  {
+    'key'         => 'radius-ip',
+    'section'     => '',
+    'description' => 'RADIUS attribute for IP addresses.',
+    'type'        => 'select',
+    'select_enum' => [ 'Framed-IP-Address', 'Framed-Address' ],
+  },
+
+  {
+    'key'         => 'svc_acct-alldomains',
+    'section'     => '',
+    'description' => 'Allow accounts to select any domain in the database.  Normally accounts can only select from the domain set in the service definition and those purchased by the customer.',
+    'type'        => 'checkbox',
+  },
+
+  {
+    'key'         => 'dump-scpdest',
+    'section'     => '',
+    'description' => 'destination for scp database dumps: user@host:/path',
+    'type'        => 'text',
+  },
+
+);
+
+1;
+
diff --git a/FS/FS/ConfItem.pm b/FS/FS/ConfItem.pm
new file mode 100644 (file)
index 0000000..83295b4
--- /dev/null
@@ -0,0 +1,63 @@
+package FS::ConfItem;
+
+=head1 NAME
+
+FS::ConfItem - Configutaion option meta-data.
+
+=head1 SYNOPSIS
+
+  use FS::Conf;
+  @config_items = $conf->config_items;
+
+  foreach $item ( @config_items ) {
+    $key = $item->key;
+    $section = $item->section;
+    $description = $item->description;
+  }
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=over 4
+
+=item new
+
+=cut
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = @_ ? shift : {};
+  bless ($self, $class);
+}
+
+=item key
+
+=item section
+
+=item description
+
+=cut
+
+sub AUTOLOAD {
+  my $self = shift;
+  my $field = $AUTOLOAD;
+  $field =~ s/.*://;
+  $self->{$field};
+}
+
+=back
+
+=head1 BUGS
+
+Terse docs.
+
+=head1 SEE ALSO
+
+L<FS::Conf>
+
+=cut
+
+1;
+
diff --git a/FS/FS/InitHandler.pm b/FS/FS/InitHandler.pm
new file mode 100644 (file)
index 0000000..5038cf3
--- /dev/null
@@ -0,0 +1,91 @@
+package FS::InitHandler;
+
+# this leaks memory under graceful restarts and i wouldn't use it on any
+# modern server.  useful for very slow machines with memory to spare, just
+# always do a full restart
+
+use strict;
+use vars qw($DEBUG);
+use FS::UID qw(adminsuidsetup);
+use FS::Record;
+
+$DEBUG = 1;
+
+sub handler {
+
+  use Date::Format;
+  use Date::Parse;
+  use Tie::IxHash;
+  use HTML::Entities;
+  use IO::Handle;
+  use IO::File;
+  use String::Approx;
+  use HTML::Widgets::SelectLayers 0.02;
+  #use FS::UID;
+  #use FS::Record;
+  use FS::Conf;
+  use FS::CGI;
+  use FS::Msgcat;
+  
+  use FS::agent;
+  use FS::agent_type;
+  use FS::domain_record;
+  use FS::cust_bill;
+  use FS::cust_bill_pay;
+  use FS::cust_credit;
+  use FS::cust_credit_bill;
+  use FS::cust_main;
+  use FS::cust_main_county;
+  use FS::cust_pay;
+  use FS::cust_pkg;
+  use FS::cust_refund;
+  use FS::cust_svc;
+  use FS::nas;
+  use FS::part_bill_event;
+  use FS::part_pkg;
+  use FS::part_referral;
+  use FS::part_svc;
+  use FS::pkg_svc;
+  use FS::port;
+  use FS::queue;
+  use FS::raddb;
+  use FS::session;
+  use FS::svc_acct;
+  use FS::svc_acct_pop;
+  use FS::svc_domain;
+  use FS::svc_forward;
+  use FS::svc_www;
+  use FS::type_pkgs;
+  use FS::part_export;
+  use FS::part_export_option;
+  use FS::export_svc;
+  use FS::msgcat;
+
+  warn "[FS::InitHandler] handler called\n" if $DEBUG;
+
+  #this is sure to be broken on freebsd
+  $> = $FS::UID::freeside_uid;
+
+  open(MAPSECRETS,"<$FS::UID::conf_dir/mapsecrets")
+    or die "can't read $FS::UID::conf_dir/mapsecrets: $!";
+
+  my %seen;
+  while (<MAPSECRETS>) {
+    next if /^\s*(#|$)/;
+    /^([\w\-\.]+)\s(.*)$/
+      or do { warn "strange line in mapsecrets: $_"; next; };
+    my($user, $datasrc) = ($1, $2);
+    next if $seen{$datasrc}++;
+    warn "[FS::InitHandler] preloading $datasrc for $user\n" if $DEBUG;
+    adminsuidsetup($user);
+  }
+
+  close MAPSECRETS;
+
+  #lalala probably broken on freebsd
+  ($<, $>) = ($>, $<);
+  $< = 0;
+
+}
+
+1;
diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm
new file mode 100644 (file)
index 0000000..56dc72e
--- /dev/null
@@ -0,0 +1,95 @@
+package FS::Misc;
+
+use strict;
+use vars qw ( @ISA @EXPORT_OK );
+use Exporter;
+
+@ISA = qw( Exporter );
+@EXPORT_OK = qw( send_email );
+
+=head1 NAME
+
+FS::Misc - Miscellaneous subroutines
+
+=head1 SYNOPSIS
+
+  use FS::Misc qw(send_email);
+
+  send_email();
+
+=head1 DESCRIPTION
+
+Miscellaneous subroutines.  This module contains miscellaneous subroutines
+called from multiple other modules.  These are not OO or necessarily related,
+but are collected here to elimiate code duplication.
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item send_email OPTION => VALUE ...
+
+Options:
+
+I<from> - (required)
+
+I<to> - (required) comma-separated scalar or arrayref of recipients
+
+I<subject> - (required)
+
+I<content-type> - (optional) MIME type
+
+I<body> - (required) arrayref of body text lines
+
+=cut
+
+use vars qw( $conf );
+use Date::Format;
+use Mail::Header;
+use Mail::Internet 1.44;
+use FS::UID;
+
+FS::UID->install_callback( sub {
+  $conf = new FS::Conf;
+} );
+
+sub send_email {
+  my(%options) = shift;
+
+  $ENV{MAILADDRESS} = $options{'from'};
+  my $to = ref($options{to}) ? join(', ', @{ $options{to} } ) : $options{to};
+  my @header = (
+    'From: '.     $options{'from'},
+    'To: '.       $to,
+    'Sender: '.   $options{'from'},
+    'Reply-To: '. $options{'from'},
+    'Date: '.     time2str("%a, %d %b %Y %X %z", time),
+    'Subject: '.  $options{'subject'},
+  );
+  push @header, 'Content-Type: '. $options{'content-type'}
+    if exists($options{'content-type'});
+  my $header = new Mail::Header ( \@header );
+
+  my $message = new Mail::Internet (
+    'Header' => $header,
+    'Body'   => $options{'body'},
+  );
+
+  my $smtpmachine = $conf->config('smtpmachine');
+  $!=0;
+  $message->smtpsend( 'Host' => $smtpmachine )
+    or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
+      or return "can't send email to $to via server $smtpmachine with SMTP: $!";
+}
+
+=head1 BUGS
+
+This package exists.
+
+=head1 SEE ALSO
+
+L<FS::UID>, L<FS::CGI>, L<FS::Record>, the base documentation.
+
+=cut
+
+1;
diff --git a/FS/FS/Msgcat.pm b/FS/FS/Msgcat.pm
new file mode 100644 (file)
index 0000000..625743d
--- /dev/null
@@ -0,0 +1,98 @@
+package FS::Msgcat;
+
+use strict;
+use vars qw( @ISA @EXPORT_OK $conf $locale $debug );
+use Exporter;
+use FS::UID;
+#use FS::Record qw( qsearchs ); # wtf?  won't import...
+use FS::Record;
+use FS::Conf;
+use FS::msgcat;
+
+@ISA = qw(Exporter);
+@EXPORT_OK = qw( gettext geterror );
+
+$FS::UID::callback{'Msgcat'} = sub {
+  $conf = new FS::Conf;
+  $locale = $conf->config('locale') || 'en_US';
+  $debug = $conf->exists('show-msgcat-codes')
+};
+
+=head1 NAME
+
+FS::Msgcat - Message catalog functions
+
+=head1 SYNOPSIS
+
+  use FS::Msgcat qw(gettext geterror);
+
+  #simple interface for retreiving messages...
+  $message = gettext('msgcode');
+  #or errors (includes the error code)
+  $message = geterror('msgcode');
+
+=head1 DESCRIPTION
+
+FS::Msgcat provides functions to use the message catalog.  If you want to
+maintain the message catalog database, see L<FS::msgcat> instead.
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item gettext MSGCODE
+
+Returns the full message for the supplied message code.
+
+=cut
+
+sub gettext {
+  $debug ? geterror(@_) : _gettext(@_);
+}
+
+sub _gettext {
+  my $msgcode = shift;
+  my $msgcat = FS::Record::qsearchs('msgcat', {
+    'msgcode' => $msgcode,
+    'locale' => $locale
+  } );
+  if ( $msgcat ) {
+    $msgcat->msg;
+  } else {
+    warn "WARNING: message for msgcode $msgcode in locale $locale not found";
+    $msgcode;
+  }
+
+}
+
+=item geterror MSGCODE
+
+Returns the full message for the supplied message code, including the message
+code.
+
+=cut
+
+sub geterror {
+  my $msgcode = shift;
+  my $msg = _gettext($msgcode);
+  if ( $msg eq $msgcode ) {
+    "Error code $msgcode (message for locale $locale not found)";
+  } else {
+    "$msg (error code $msgcode)";
+  }
+}
+
+=back
+
+=head1 BUGS
+
+i18n/l10n, eek
+
+=head1 SEE ALSO
+
+L<FS::msgcat>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
new file mode 100644 (file)
index 0000000..02fd4e3
--- /dev/null
@@ -0,0 +1,1346 @@
+package FS::Record;
+
+use strict;
+use vars qw( $dbdef_file $dbdef $setup_hack $AUTOLOAD @ISA @EXPORT_OK $DEBUG
+             $me %dbdef_cache );
+use subs qw(reload_dbdef);
+use Exporter;
+use Carp qw(carp cluck croak confess);
+use File::CounterFile;
+use Locale::Country;
+use DBI qw(:sql_types);
+use DBIx::DBSchema 0.21;
+use FS::UID qw(dbh getotaker datasrc driver_name);
+use FS::SearchCache;
+use FS::Msgcat qw(gettext);
+
+@ISA = qw(Exporter);
+@EXPORT_OK = qw(dbh fields hfields qsearch qsearchs dbdef jsearch);
+
+$DEBUG = 0;
+$me = '[FS::Record]';
+
+#ask FS::UID to run this stuff for us later
+$FS::UID::callback{'FS::Record'} = sub { 
+  $File::CounterFile::DEFAULT_DIR = "/usr/local/etc/freeside/counters.". datasrc;
+  $dbdef_file = "/usr/local/etc/freeside/dbdef.". datasrc;
+  &reload_dbdef unless $setup_hack; #$setup_hack needed now?
+};
+
+=head1 NAME
+
+FS::Record - Database record objects
+
+=head1 SYNOPSIS
+
+    use FS::Record;
+    use FS::Record qw(dbh fields qsearch qsearchs dbdef);
+
+    $record = new FS::Record 'table', \%hash;
+    $record = new FS::Record 'table', { 'column' => 'value', ... };
+
+    $record  = qsearchs FS::Record 'table', \%hash;
+    $record  = qsearchs FS::Record 'table', { 'column' => 'value', ... };
+    @records = qsearch  FS::Record 'table', \%hash; 
+    @records = qsearch  FS::Record 'table', { 'column' => 'value', ... };
+
+    $table = $record->table;
+    $dbdef_table = $record->dbdef_table;
+
+    $value = $record->get('column');
+    $value = $record->getfield('column');
+    $value = $record->column;
+
+    $record->set( 'column' => 'value' );
+    $record->setfield( 'column' => 'value' );
+    $record->column('value');
+
+    %hash = $record->hash;
+
+    $hashref = $record->hashref;
+
+    $error = $record->insert;
+
+    $error = $record->delete;
+
+    $error = $new_record->replace($old_record);
+
+    # external use deprecated - handled by the database (at least for Pg, mysql)
+    $value = $record->unique('column');
+
+    $error = $record->ut_float('column');
+    $error = $record->ut_number('column');
+    $error = $record->ut_numbern('column');
+    $error = $record->ut_money('column');
+    $error = $record->ut_text('column');
+    $error = $record->ut_textn('column');
+    $error = $record->ut_alpha('column');
+    $error = $record->ut_alphan('column');
+    $error = $record->ut_phonen('column');
+    $error = $record->ut_anything('column');
+    $error = $record->ut_name('column');
+
+    $dbdef = reload_dbdef;
+    $dbdef = reload_dbdef "/non/standard/filename";
+    $dbdef = dbdef;
+
+    $quoted_value = _quote($value,'table','field');
+
+    #deprecated
+    $fields = hfields('table');
+    if ( $fields->{Field} ) { # etc.
+
+    @fields = fields 'table'; #as a subroutine
+    @fields = $record->fields; #as a method call
+
+
+=head1 DESCRIPTION
+
+(Mostly) object-oriented interface to database records.  Records are currently
+implemented on top of DBI.  FS::Record is intended as a base class for
+table-specific classes to inherit from, i.e. FS::cust_main.
+
+=head1 CONSTRUCTORS
+
+=over 4
+
+=item new [ TABLE, ] HASHREF
+
+Creates a new record.  It doesn't store it in the database, though.  See
+L<"insert"> for that.
+
+Note that the object stores this hash reference, not a distinct copy of the
+hash it points to.  You can ask the object for a copy with the I<hash> 
+method.
+
+TABLE can only be omitted when a dervived class overrides the table method.
+
+=cut
+
+sub new { 
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = {};
+  bless ($self, $class);
+
+  unless ( defined ( $self->table ) ) {
+    $self->{'Table'} = shift;
+    carp "warning: FS::Record::new called with table name ". $self->{'Table'};
+  }
+
+  my $hashref = $self->{'Hash'} = shift;
+
+  foreach my $field ( grep !defined($hashref->{$_}), $self->fields ) { 
+    $hashref->{$field}='';
+  }
+
+  $self->_cache($hashref, shift) if $self->can('_cache') && @_;
+
+  $self;
+}
+
+sub new_or_cached {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = {};
+  bless ($self, $class);
+
+  $self->{'Table'} = shift unless defined ( $self->table );
+
+  my $hashref = $self->{'Hash'} = shift;
+  my $cache = shift;
+  if ( defined( $cache->cache->{$hashref->{$cache->key}} ) ) {
+    my $obj = $cache->cache->{$hashref->{$cache->key}};
+    $obj->_cache($hashref, $cache) if $obj->can('_cache');
+    $obj;
+  } else {
+    $cache->cache->{$hashref->{$cache->key}} = $self->new($hashref, $cache);
+  }
+
+}
+
+sub create {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = {};
+  bless ($self, $class);
+  if ( defined $self->table ) {
+    cluck "create constructor is deprecated, use new!";
+    $self->new(@_);
+  } else {
+    croak "FS::Record::create called (not from a subclass)!";
+  }
+}
+
+=item qsearch TABLE, HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ
+
+Searches the database for all records matching (at least) the key/value pairs
+in HASHREF.  Returns all the records found as `FS::TABLE' objects if that
+module is loaded (i.e. via `use FS::cust_main;'), otherwise returns FS::Record
+objects.
+
+###oops, argh, FS::Record::new only lets us create database fields.
+#Normal behaviour if SELECT is not specified is `*', as in
+#C<SELECT * FROM table WHERE ...>.  However, there is an experimental new
+#feature where you can specify SELECT - remember, the objects returned,
+#although blessed into the appropriate `FS::TABLE' package, will only have the
+#fields you specify.  This might have unwanted results if you then go calling
+#regular FS::TABLE methods
+#on it.
+
+=cut
+
+sub qsearch {
+  my($stable, $record, $select, $extra_sql, $cache ) = @_;
+  #$stable =~ /^([\w\_]+)$/ or die "Illegal table: $table";
+  #for jsearch
+  $stable =~ /^([\w\s\(\)\.\,\=]+)$/ or die "Illegal table: $stable";
+  $stable = $1;
+  $select ||= '*';
+  my $dbh = dbh;
+
+  my $table = $cache ? $cache->table : $stable;
+
+  my @fields = grep exists($record->{$_}), fields($table);
+
+  my $statement = "SELECT $select FROM $stable";
+  if ( @fields ) {
+    $statement .= ' WHERE '. join(' AND ', map {
+
+      my $op = '=';
+      my $column = $_;
+      if ( ref($record->{$_}) ) {
+        $op = $record->{$_}{'op'} if $record->{$_}{'op'};
+        #$op = 'LIKE' if $op =~ /^ILIKE$/i && driver_name ne 'Pg';
+        if ( uc($op) eq 'ILIKE' ) {
+          $op = 'LIKE';
+          $record->{$_}{'value'} = lc($record->{$_}{'value'});
+          $column = "LOWER($_)";
+        }
+        $record->{$_} = $record->{$_}{'value'}
+      }
+
+      if ( ! defined( $record->{$_} ) || $record->{$_} eq '' ) {
+        if ( $op eq '=' ) {
+          if ( driver_name eq 'Pg' ) {
+            if ( $dbdef->table($table)->column($column)->type =~ /(int)/i ) {
+              qq-( $column IS NULL )-;
+            } else {
+              qq-( $column IS NULL OR $column = '' )-;
+            }
+          } else {
+            qq-( $column IS NULL OR $column = "" )-;
+          }
+        } elsif ( $op eq '!=' ) {
+          if ( driver_name eq 'Pg' ) {
+            if ( $dbdef->table($table)->column($column)->type =~ /(int)/i ) {
+              qq-( $column IS NOT NULL )-;
+            } else {
+              qq-( $column IS NOT NULL AND $column != '' )-;
+            }
+          } else {
+            qq-( $column IS NOT NULL AND $column != "" )-;
+          }
+        } else {
+          if ( driver_name eq 'Pg' ) {
+            qq-( $column $op '' )-;
+          } else {
+            qq-( $column $op "" )-;
+          }
+        }
+      } else {
+        "$column $op ?";
+      }
+    } @fields );
+  }
+  $statement .= " $extra_sql" if defined($extra_sql);
+
+  warn "[debug]$me $statement\n" if $DEBUG > 1;
+  my $sth = $dbh->prepare($statement)
+    or croak "$dbh->errstr doing $statement";
+
+  my $bind = 1;
+
+  foreach my $field (
+    grep defined( $record->{$_} ) && $record->{$_} ne '', @fields
+  ) {
+    if ( $record->{$field} =~ /^\d+(\.\d+)?$/
+         && $dbdef->table($table)->column($field)->type =~ /(int)/i
+    ) {
+      $sth->bind_param($bind++, $record->{$field}, { TYPE => SQL_INTEGER } );
+    } else {
+      $sth->bind_param($bind++, $record->{$field}, { TYPE => SQL_VARCHAR } );
+    }
+  }
+
+#  $sth->execute( map $record->{$_},
+#    grep defined( $record->{$_} ) && $record->{$_} ne '', @fields
+#  ) or croak "Error executing \"$statement\": ". $sth->errstr;
+
+  $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr;
+
+  $dbh->commit or croak $dbh->errstr if $FS::UID::AutoCommit;
+
+  if ( eval 'scalar(@FS::'. $table. '::ISA);' ) {
+    if ( eval 'FS::'. $table. '->can(\'new\')' eq \&new ) {
+      #derivied class didn't override new method, so this optimization is safe
+      if ( $cache ) {
+        map {
+          new_or_cached( "FS::$table", { %{$_} }, $cache )
+        } @{$sth->fetchall_arrayref( {} )};
+      } else {
+        map {
+          new( "FS::$table", { %{$_} } )
+        } @{$sth->fetchall_arrayref( {} )};
+      }
+    } else {
+      warn "untested code (class FS::$table uses custom new method)";
+      map {
+        eval 'FS::'. $table. '->new( { %{$_} } )';
+      } @{$sth->fetchall_arrayref( {} )};
+    }
+  } else {
+    cluck "warning: FS::$table not loaded; returning FS::Record objects";
+    map {
+      FS::Record->new( $table, { %{$_} } );
+    } @{$sth->fetchall_arrayref( {} )};
+  }
+
+}
+
+=item jsearch TABLE, HASHREF, SELECT, EXTRA_SQL, PRIMARY_TABLE, PRIMARY_KEY
+
+Experimental JOINed search method.  Using this method, you can execute a
+single SELECT spanning multiple tables, and cache the results for subsequent
+method calls.  Interface will almost definately change in an incompatible
+fashion.
+
+Arguments: 
+
+=cut
+
+sub jsearch {
+  my($table, $record, $select, $extra_sql, $ptable, $pkey ) = @_;
+  my $cache = FS::SearchCache->new( $ptable, $pkey );
+  my %saw;
+  ( $cache,
+    grep { !$saw{$_->getfield($pkey)}++ }
+      qsearch($table, $record, $select, $extra_sql, $cache )
+  );
+}
+
+=item qsearchs TABLE, HASHREF
+
+Same as qsearch, except that if more than one record matches, it B<carp>s but
+returns the first.  If this happens, you either made a logic error in asking
+for a single item, or your data is corrupted.
+
+=cut
+
+sub qsearchs { # $result_record = &FS::Record:qsearchs('table',\%hash);
+  my $table = $_[0];
+  my(@result) = qsearch(@_);
+  carp "warning: Multiple records in scalar search ($table)"
+    if scalar(@result) > 1;
+  #should warn more vehemently if the search was on a primary key?
+  scalar(@result) ? ($result[0]) : ();
+}
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item table
+
+Returns the table name.
+
+=cut
+
+sub table {
+#  cluck "warning: FS::Record::table deprecated; supply one in subclass!";
+  my $self = shift;
+  $self -> {'Table'};
+}
+
+=item dbdef_table
+
+Returns the DBIx::DBSchema::Table object for the table.
+
+=cut
+
+sub dbdef_table {
+  my($self)=@_;
+  my($table)=$self->table;
+  $dbdef->table($table);
+}
+
+=item get, getfield COLUMN
+
+Returns the value of the column/field/key COLUMN.
+
+=cut
+
+sub get {
+  my($self,$field) = @_;
+  # to avoid "Use of unitialized value" errors
+  if ( defined ( $self->{Hash}->{$field} ) ) {
+    $self->{Hash}->{$field};
+  } else { 
+    '';
+  }
+}
+sub getfield {
+  my $self = shift;
+  $self->get(@_);
+}
+
+=item set, setfield COLUMN, VALUE
+
+Sets the value of the column/field/key COLUMN to VALUE.  Returns VALUE.
+
+=cut
+
+sub set { 
+  my($self,$field,$value) = @_;
+  $self->{'Hash'}->{$field} = $value;
+}
+sub setfield {
+  my $self = shift;
+  $self->set(@_);
+}
+
+=item AUTLOADED METHODS
+
+$record->column is a synonym for $record->get('column');
+
+$record->column('value') is a synonym for $record->set('column','value');
+
+=cut
+
+# readable/safe
+sub AUTOLOAD {
+  my($self,$value)=@_;
+  my($field)=$AUTOLOAD;
+  $field =~ s/.*://;
+  if ( defined($value) ) {
+    confess "errant AUTOLOAD $field for $self (arg $value)"
+      unless ref($self) && $self->can('setfield');
+    $self->setfield($field,$value);
+  } else {
+    confess "errant AUTOLOAD $field for $self (no args)"
+      unless ref($self) && $self->can('getfield');
+    $self->getfield($field);
+  }    
+}
+
+# efficient
+#sub AUTOLOAD {
+#  my $field = $AUTOLOAD;
+#  $field =~ s/.*://;
+#  if ( defined($_[1]) ) {
+#    $_[0]->setfield($field, $_[1]);
+#  } else {
+#    $_[0]->getfield($field);
+#  }    
+#}
+
+=item hash
+
+Returns a list of the column/value pairs, usually for assigning to a new hash.
+
+To make a distinct duplicate of an FS::Record object, you can do:
+
+    $new = new FS::Record ( $old->table, { $old->hash } );
+
+=cut
+
+sub hash {
+  my($self) = @_;
+  %{ $self->{'Hash'} }; 
+}
+
+=item hashref
+
+Returns a reference to the column/value hash.
+
+=cut
+
+sub hashref {
+  my($self) = @_;
+  $self->{'Hash'};
+}
+
+=item insert
+
+Inserts this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  my $error = $self->check;
+  return $error if $error;
+
+  #single-field unique keys are given a value if false
+  #(like MySQL's AUTO_INCREMENT or Pg SERIAL)
+  foreach ( $self->dbdef_table->unique->singles ) {
+    $self->unique($_) unless $self->getfield($_);
+  }
+
+  #and also the primary key, if the database isn't going to
+  my $primary_key = $self->dbdef_table->primary_key;
+  my $db_seq = 0;
+  if ( $primary_key ) {
+    my $col = $self->dbdef_table->column($primary_key);
+    
+    $db_seq =
+      uc($col->type) eq 'SERIAL'
+      || ( driver_name eq 'Pg'
+             && defined($col->default)
+             && $col->default =~ /^nextval\(/i
+         )
+      || ( driver_name eq 'mysql'
+             && defined($col->local)
+             && $col->local =~ /AUTO_INCREMENT/i
+         );
+    $self->unique($primary_key) unless $self->getfield($primary_key) || $db_seq;
+  }
+
+  my $table = $self->table;
+  #false laziness w/delete
+  my @fields =
+    grep defined($self->getfield($_)) && $self->getfield($_) ne "",
+    $self->fields
+  ;
+  my @values = map { _quote( $self->getfield($_), $table, $_) } @fields;
+  #eslaf
+
+  my $statement = "INSERT INTO $table ( ".
+      join( ', ', @fields ).
+    ") VALUES (".
+      join( ', ', @values ).
+    ")"
+  ;
+  warn "[debug]$me $statement\n" if $DEBUG > 1;
+  my $sth = dbh->prepare($statement) or return dbh->errstr;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE'; 
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  $sth->execute or return $sth->errstr;
+
+  if ( $db_seq ) { # get inserted id from the database, if applicable
+    warn "[debug]$me retreiving sequence from database\n" if $DEBUG;
+    my $insertid = '';
+    if ( driver_name eq 'Pg' ) {
+
+      my $oid = $sth->{'pg_oid_status'};
+      my $i_sql = "SELECT $primary_key FROM $table WHERE oid = ?";
+      my $i_sth = dbh->prepare($i_sql) or do {
+        dbh->rollback if $FS::UID::AutoCommit;
+        return dbh->errstr;
+      };
+      $i_sth->execute($oid) or do {
+        dbh->rollback if $FS::UID::AutoCommit;
+        return $i_sth->errstr;
+      };
+      $insertid = $i_sth->fetchrow_arrayref->[0];
+
+    } elsif ( driver_name eq 'mysql' ) {
+
+      $insertid = dbh->{'mysql_insertid'};
+      # work around mysql_insertid being null some of the time, ala RT :/
+      unless ( $insertid ) {
+        warn "WARNING: DBD::mysql didn't return mysql_insertid; ".
+             "using SELECT LAST_INSERT_ID();";
+        my $i_sql = "SELECT LAST_INSERT_ID()";
+        my $i_sth = dbh->prepare($i_sql) or do {
+          dbh->rollback if $FS::UID::AutoCommit;
+          return dbh->errstr;
+        };
+        $i_sth->execute or do {
+          dbh->rollback if $FS::UID::AutoCommit;
+          return $i_sth->errstr;
+        };
+        $insertid = $i_sth->fetchrow_arrayref->[0];
+      }
+
+    } else {
+      dbh->rollback if $FS::UID::AutoCommit;
+      return "don't know how to retreive inserted ids from ". driver_name. 
+             ", try using counterfiles (maybe run dbdef-create?)";
+    }
+    $self->setfield($primary_key, $insertid);
+  }
+
+  my $h_sth;
+  if ( defined $dbdef->table('h_'. $table) ) {
+    my $h_statement = $self->_h_statement('insert');
+    warn "[debug]$me $h_statement\n" if $DEBUG > 2;
+    $h_sth = dbh->prepare($h_statement) or do {
+      dbh->rollback if $FS::UID::AutoCommit;
+      return dbh->errstr;
+    };
+  } else {
+    $h_sth = '';
+  }
+  $h_sth->execute or return $h_sth->errstr if $h_sth;
+
+  dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
+
+  '';
+}
+
+=item add
+
+Depriciated (use insert instead).
+
+=cut
+
+sub add {
+  cluck "warning: FS::Record::add deprecated!";
+  insert @_; #call method in this scope
+}
+
+=item delete
+
+Delete this record from the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub delete {
+  my $self = shift;
+
+  my $statement = "DELETE FROM ". $self->table. " WHERE ". join(' AND ',
+    map {
+      $self->getfield($_) eq ''
+        #? "( $_ IS NULL OR $_ = \"\" )"
+        ? ( driver_name eq 'Pg'
+              ? "$_ IS NULL"
+              : "( $_ IS NULL OR $_ = \"\" )"
+          )
+        : "$_ = ". _quote($self->getfield($_),$self->table,$_)
+    } ( $self->dbdef_table->primary_key )
+          ? ( $self->dbdef_table->primary_key)
+          : $self->fields
+  );
+  warn "[debug]$me $statement\n" if $DEBUG > 1;
+  my $sth = dbh->prepare($statement) or return dbh->errstr;
+
+  my $h_sth;
+  if ( defined $dbdef->table('h_'. $self->table) ) {
+    my $h_statement = $self->_h_statement('delete');
+    warn "[debug]$me $h_statement\n" if $DEBUG > 2;
+    $h_sth = dbh->prepare($h_statement) or return dbh->errstr;
+  } else {
+    $h_sth = '';
+  }
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE'; 
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $rc = $sth->execute or return $sth->errstr;
+  #not portable #return "Record not found, statement:\n$statement" if $rc eq "0E0";
+  $h_sth->execute or return $h_sth->errstr if $h_sth;
+  dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
+
+  #no need to needlessly destoy the data either (causes problems actually)
+  #undef $self; #no need to keep object!
+
+  '';
+}
+
+=item del
+
+Depriciated (use delete instead).
+
+=cut
+
+sub del {
+  cluck "warning: FS::Record::del deprecated!";
+  &delete(@_); #call method in this scope
+}
+
+=item replace OLD_RECORD
+
+Replace the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+  warn "[debug]$me $new ->replace $old\n" if $DEBUG;
+
+  return "Records not in same table!" unless $new->table eq $old->table;
+
+  my $primary_key = $old->dbdef_table->primary_key;
+  return "Can't change $primary_key"
+    if $primary_key
+       && ( $old->getfield($primary_key) ne $new->getfield($primary_key) );
+
+  my $error = $new->check;
+  return $error if $error;
+
+  my @diff = grep $new->getfield($_) ne $old->getfield($_), $old->fields;
+  unless ( @diff ) {
+    carp "[warning]$me $new -> replace $old: records identical";
+    return '';
+  }
+
+  my $statement = "UPDATE ". $old->table. " SET ". join(', ',
+    map {
+      "$_ = ". _quote($new->getfield($_),$old->table,$_) 
+    } @diff
+  ). ' WHERE '.
+    join(' AND ',
+      map {
+        $old->getfield($_) eq ''
+          #? "( $_ IS NULL OR $_ = \"\" )"
+          ? ( driver_name eq 'Pg'
+                ? "$_ IS NULL"
+                : "( $_ IS NULL OR $_ = \"\" )"
+            )
+          : "$_ = ". _quote($old->getfield($_),$old->table,$_)
+      } ( $primary_key ? ( $primary_key ) : $old->fields )
+    )
+  ;
+  warn "[debug]$me $statement\n" if $DEBUG > 1;
+  my $sth = dbh->prepare($statement) or return dbh->errstr;
+
+  my $h_old_sth;
+  if ( defined $dbdef->table('h_'. $old->table) ) {
+    my $h_old_statement = $old->_h_statement('replace_old');
+    warn "[debug]$me $h_old_statement\n" if $DEBUG > 2;
+    $h_old_sth = dbh->prepare($h_old_statement) or return dbh->errstr;
+  } else {
+    $h_old_sth = '';
+  }
+
+  my $h_new_sth;
+  if ( defined $dbdef->table('h_'. $new->table) ) {
+    my $h_new_statement = $new->_h_statement('replace_new');
+    warn "[debug]$me $h_new_statement\n" if $DEBUG > 2;
+    $h_new_sth = dbh->prepare($h_new_statement) or return dbh->errstr;
+  } else {
+    $h_new_sth = '';
+  }
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE'; 
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $rc = $sth->execute or return $sth->errstr;
+  #not portable #return "Record not found (or records identical)." if $rc eq "0E0";
+  $h_old_sth->execute or return $h_old_sth->errstr if $h_old_sth;
+  $h_new_sth->execute or return $h_new_sth->errstr if $h_new_sth;
+  dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
+
+  '';
+
+}
+
+=item rep
+
+Depriciated (use replace instead).
+
+=cut
+
+sub rep {
+  cluck "warning: FS::Record::rep deprecated!";
+  replace @_; #call method in this scope
+}
+
+=item check
+
+Not yet implemented, croaks.  Derived classes should provide a check method.
+
+=cut
+
+sub check {
+  confess "FS::Record::check not implemented; supply one in subclass!";
+}
+
+sub _h_statement {
+  my( $self, $action ) = @_;
+
+  my @fields =
+    grep defined($self->getfield($_)) && $self->getfield($_) ne "",
+    $self->fields
+  ;
+  my @values = map { _quote( $self->getfield($_), $self->table, $_) } @fields;
+
+  "INSERT INTO h_". $self->table. " ( ".
+      join(', ', qw(history_date history_user history_action), @fields ).
+    ") VALUES (".
+      join(', ', time, dbh->quote(getotaker()), dbh->quote($action), @values).
+    ")"
+  ;
+}
+
+=item unique COLUMN
+
+B<Warning>: External use is B<deprecated>.  
+
+Replaces COLUMN in record with a unique number, using counters in the
+filesystem.  Used by the B<insert> method on single-field unique columns
+(see L<DBIx::DBSchema::Table>) and also as a fallback for primary keys
+that aren't SERIAL (Pg) or AUTO_INCREMENT (mysql).
+
+Returns the new value.
+
+=cut
+
+sub unique {
+  my($self,$field) = @_;
+  my($table)=$self->table;
+
+  croak "Unique called on field $field, but it is ",
+        $self->getfield($field),
+        ", not null!"
+    if $self->getfield($field);
+
+  #warn "table $table is tainted" if is_tainted($table);
+  #warn "field $field is tainted" if is_tainted($field);
+
+  my($counter) = new File::CounterFile "$table.$field",0;
+# hack for web demo
+#  getotaker() =~ /^([\w\-]{1,16})$/ or die "Illegal CGI REMOTE_USER!";
+#  my($user)=$1;
+#  my($counter) = new File::CounterFile "$user/$table.$field",0;
+# endhack
+
+  my $index = $counter->inc;
+  $index = $counter->inc while qsearchs($table, { $field=>$index } );
+
+  $index =~ /^(\d*)$/;
+  $index=$1;
+
+  $self->setfield($field,$index);
+
+}
+
+=item ut_float COLUMN
+
+Check/untaint floating point numeric data: 1.1, 1, 1.1e10, 1e10.  May not be
+null.  If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_float {
+  my($self,$field)=@_ ;
+  ($self->getfield($field) =~ /^(\d+\.\d+)$/ ||
+   $self->getfield($field) =~ /^(\d+)$/ ||
+   $self->getfield($field) =~ /^(\d+\.\d+e\d+)$/ ||
+   $self->getfield($field) =~ /^(\d+e\d+)$/)
+    or return "Illegal or empty (float) $field: ". $self->getfield($field);
+  $self->setfield($field,$1);
+  '';
+}
+
+=item ut_number COLUMN
+
+Check/untaint simple numeric data (whole numbers).  May not be null.  If there
+is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_number {
+  my($self,$field)=@_;
+  $self->getfield($field) =~ /^(\d+)$/
+    or return "Illegal or empty (numeric) $field: ". $self->getfield($field);
+  $self->setfield($field,$1);
+  '';
+}
+
+=item ut_numbern COLUMN
+
+Check/untaint simple numeric data (whole numbers).  May be null.  If there is
+an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_numbern {
+  my($self,$field)=@_;
+  $self->getfield($field) =~ /^(\d*)$/
+    or return "Illegal (numeric) $field: ". $self->getfield($field);
+  $self->setfield($field,$1);
+  '';
+}
+
+=item ut_money COLUMN
+
+Check/untaint monetary numbers.  May be negative.  Set to 0 if null.  If there
+is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_money {
+  my($self,$field)=@_;
+  $self->setfield($field, 0) if $self->getfield($field) eq '';
+  $self->getfield($field) =~ /^(\-)? ?(\d*)(\.\d{2})?$/
+    or return "Illegal (money) $field: ". $self->getfield($field);
+  #$self->setfield($field, "$1$2$3" || 0);
+  $self->setfield($field, ( ($1||''). ($2||''). ($3||'') ) || 0);
+  '';
+}
+
+=item ut_text COLUMN
+
+Check/untaint text.  Alphanumerics, spaces, and the following punctuation
+symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? / =
+May not be null.  If there is an error, returns the error, otherwise returns
+false.
+
+=cut
+
+sub ut_text {
+  my($self,$field)=@_;
+  #warn "msgcat ". \&msgcat. "\n";
+  #warn "notexist ". \&notexist. "\n";
+  #warn "AUTOLOAD ". \&AUTOLOAD. "\n";
+  $self->getfield($field) =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]+)$/
+    or return gettext('illegal_or_empty_text'). " $field: ".
+               $self->getfield($field);
+  $self->setfield($field,$1);
+  '';
+}
+
+=item ut_textn COLUMN
+
+Check/untaint text.  Alphanumerics, spaces, and the following punctuation
+symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? /
+May be null.  If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_textn {
+  my($self,$field)=@_;
+  $self->getfield($field) =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
+    or return gettext('illegal_text'). " $field: ". $self->getfield($field);
+  $self->setfield($field,$1);
+  '';
+}
+
+=item ut_alpha COLUMN
+
+Check/untaint alphanumeric strings (no spaces).  May not be null.  If there is
+an error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_alpha {
+  my($self,$field)=@_;
+  $self->getfield($field) =~ /^(\w+)$/
+    or return "Illegal or empty (alphanumeric) $field: ".
+              $self->getfield($field);
+  $self->setfield($field,$1);
+  '';
+}
+
+=item ut_alpha COLUMN
+
+Check/untaint alphanumeric strings (no spaces).  May be null.  If there is an
+error, returns the error, otherwise returns false.
+
+=cut
+
+sub ut_alphan {
+  my($self,$field)=@_;
+  $self->getfield($field) =~ /^(\w*)$/ 
+    or return "Illegal (alphanumeric) $field: ". $self->getfield($field);
+  $self->setfield($field,$1);
+  '';
+}
+
+=item ut_phonen COLUMN [ COUNTRY ]
+
+Check/untaint phone numbers.  May be null.  If there is an error, returns
+the error, otherwise returns false.
+
+Takes an optional two-letter ISO country code; without it or with unsupported
+countries, ut_phonen simply calls ut_alphan.
+
+=cut
+
+sub ut_phonen {
+  my( $self, $field, $country ) = @_;
+  return $self->ut_alphan($field) unless defined $country;
+  my $phonen = $self->getfield($field);
+  if ( $phonen eq '' ) {
+    $self->setfield($field,'');
+  } elsif ( $country eq 'US' || $country eq 'CA' ) {
+    $phonen =~ s/\D//g;
+    $phonen =~ /^(\d{3})(\d{3})(\d{4})(\d*)$/
+      or return gettext('illegal_phone'). " $field: ". $self->getfield($field);
+    $phonen = "$1-$2-$3";
+    $phonen .= " x$4" if $4;
+    $self->setfield($field,$phonen);
+  } else {
+    warn "warning: don't know how to check phone numbers for country $country";
+    return $self->ut_textn($field);
+  }
+  '';
+}
+
+=item ut_ip COLUMN
+
+Check/untaint ip addresses.  IPv4 only for now.
+
+=cut
+
+sub ut_ip {
+  my( $self, $field ) = @_;
+  $self->getfield($field) =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
+    or return "Illegal (IP address) $field: ". $self->getfield($field);
+  for ( $1, $2, $3, $4 ) { return "Illegal (IP address) $field" if $_ > 255; }
+  $self->setfield($field, "$1.$2.$3.$4");
+  '';
+}
+
+=item ut_ipn COLUMN
+
+Check/untaint ip addresses.  IPv4 only for now.  May be null.
+
+=cut
+
+sub ut_ipn {
+  my( $self, $field ) = @_;
+  if ( $self->getfield($field) =~ /^()$/ ) {
+    $self->setfield($field,'');
+    '';
+  } else {
+    $self->ut_ip($field);
+  }
+}
+
+=item ut_domain COLUMN
+
+Check/untaint host and domain names.
+
+=cut
+
+sub ut_domain {
+  my( $self, $field ) = @_;
+  #$self->getfield($field) =~/^(\w+\.)*\w+$/
+  $self->getfield($field) =~/^(([\w\-]+\.)*\w+)$/
+    or return "Illegal (domain) $field: ". $self->getfield($field);
+  $self->setfield($field,$1);
+  '';
+}
+
+=item ut_name COLUMN
+
+Check/untaint proper names; allows alphanumerics, spaces and the following
+punctuation: , . - '
+
+May not be null.
+
+=cut
+
+sub ut_name {
+  my( $self, $field ) = @_;
+  $self->getfield($field) =~ /^([\w \,\.\-\']+)$/
+    or return gettext('illegal_name'). " $field: ". $self->getfield($field);
+  $self->setfield($field,$1);
+  '';
+}
+
+=item ut_zip COLUMN
+
+Check/untaint zip codes.
+
+=cut
+
+sub ut_zip {
+  my( $self, $field, $country ) = @_;
+  if ( $country eq 'US' ) {
+    $self->getfield($field) =~ /\s*(\d{5}(\-\d{4})?)\s*$/
+      or return gettext('illegal_zip'). " $field for country $country: ".
+                $self->getfield($field);
+    $self->setfield($field,$1);
+  } else {
+    $self->getfield($field) =~ /^\s*(\w[\w\-\s]{2,8}\w)\s*$/
+      or return gettext('illegal_zip'). " $field: ". $self->getfield($field);
+    $self->setfield($field,$1);
+  }
+  '';
+}
+
+=item ut_country COLUMN
+
+Check/untaint country codes.  Country names are changed to codes, if possible -
+see L<Locale::Country>.
+
+=cut
+
+sub ut_country {
+  my( $self, $field ) = @_;
+  unless ( $self->getfield($field) =~ /^(\w\w)$/ ) {
+    if ( $self->getfield($field) =~ /^([\w \,\.\(\)\']+)$/ 
+         && country2code($1) ) {
+      $self->setfield($field,uc(country2code($1)));
+    }
+  }
+  $self->getfield($field) =~ /^(\w\w)$/
+    or return "Illegal (country) $field: ". $self->getfield($field);
+  $self->setfield($field,uc($1));
+  '';
+}
+
+=item ut_anything COLUMN
+
+Untaints arbitrary data.  Be careful.
+
+=cut
+
+sub ut_anything {
+  my( $self, $field ) = @_;
+  $self->getfield($field) =~ /^(.*)$/s
+    or return "Illegal $field: ". $self->getfield($field);
+  $self->setfield($field,$1);
+  '';
+}
+
+=item ut_enum COLUMN CHOICES_ARRAYREF
+
+Check/untaint a column, supplying all possible choices, like the "enum" type.
+
+=cut
+
+sub ut_enum {
+  my( $self, $field, $choices ) = @_;
+  foreach my $choice ( @$choices ) {
+    if ( $self->getfield($field) eq $choice ) {
+      $self->setfield($choice);
+      return '';
+    }
+  }
+  return "Illegal (enum) field $field: ". $self->getfield($field);
+}
+
+=item ut_foreign_key COLUMN FOREIGN_TABLE FOREIGN_COLUMN
+
+Check/untaint a foreign column key.  Call a regular ut_ method (like ut_number)
+on the column first.
+
+=cut
+
+sub ut_foreign_key {
+  my( $self, $field, $table, $foreign ) = @_;
+  qsearchs($table, { $foreign => $self->getfield($field) })
+    or return "Can't find $field ". $self->getfield($field).
+              " in $table.$foreign";
+  '';
+}
+
+=item ut_foreign_keyn COLUMN FOREIGN_TABLE FOREIGN_COLUMN
+
+Like ut_foreign_key, except the null value is also allowed.
+
+=cut
+
+sub ut_foreign_keyn {
+  my( $self, $field, $table, $foreign ) = @_;
+  $self->getfield($field)
+    ? $self->ut_foreign_key($field, $table, $foreign)
+    : '';
+}
+
+=item fields [ TABLE ]
+
+This can be used as both a subroutine and a method call.  It returns a list
+of the columns in this record's table, or an explicitly specified table.
+(See L<DBIx::DBSchema::Table>).
+
+=cut
+
+# Usage: @fields = fields($table);
+#        @fields = $record->fields;
+sub fields {
+  my $something = shift;
+  my $table;
+  if ( ref($something) ) {
+    $table = $something->table;
+  } else {
+    $table = $something;
+  }
+  #croak "Usage: \@fields = fields(\$table)\n   or: \@fields = \$record->fields" unless $table;
+  my($table_obj) = $dbdef->table($table);
+  confess "Unknown table $table" unless $table_obj;
+  $table_obj->columns;
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item reload_dbdef([FILENAME])
+
+Load a database definition (see L<DBIx::DBSchema>), optionally from a
+non-default filename.  This command is executed at startup unless
+I<$FS::Record::setup_hack> is true.  Returns a DBIx::DBSchema object.
+
+=cut
+
+sub reload_dbdef {
+  my $file = shift || $dbdef_file;
+
+  unless ( exists $dbdef_cache{$file} ) {
+    warn "[debug]$me loading dbdef for $file\n" if $DEBUG;
+    $dbdef_cache{$file} = DBIx::DBSchema->load( $file )
+                            or die "can't load database schema from $file";
+  } else {
+    warn "[debug]$me re-using cached dbdef for $file\n" if $DEBUG;
+  }
+  $dbdef = $dbdef_cache{$file};
+}
+
+=item dbdef
+
+Returns the current database definition.  See L<DBIx::DBSchema>.
+
+=cut
+
+sub dbdef { $dbdef; }
+
+=item _quote VALUE, TABLE, COLUMN
+
+This is an internal function used to construct SQL statements.  It returns
+VALUE DBI-quoted (see L<DBI/"quote">) unless VALUE is a number and the column
+type (see L<DBIx::DBSchema::Column>) does not end in `char' or `binary'.
+
+=cut
+
+sub _quote {
+  my($value, $table, $column) = @_;
+  my $column_obj = $dbdef->table($table)->column($column);
+  my $column_type = $column_obj->type;
+
+  if ( $value eq '' && $column_type =~ /^int/ ) {
+    if ( $column_obj->null ) {
+      'NULL';
+    } else {
+      cluck "WARNING: Attempting to set non-null integer $table.$column null; ".
+            "using 0 instead";
+      0;
+    }
+  } elsif ( $value =~ /^\d+(\.\d+)?$/ && 
+            ! $column_type =~ /(char|binary|text)$/i ) {
+    $value;
+  } else {
+    dbh->quote($value);
+  }
+}
+
+=item hfields TABLE
+
+This is deprecated.  Don't use it.
+
+It returns a hash-type list with the fields of this record's table set true.
+
+=cut
+
+sub hfields {
+  carp "warning: hfields is deprecated";
+  my($table)=@_;
+  my(%hash);
+  foreach (fields($table)) {
+    $hash{$_}=1;
+  }
+  \%hash;
+}
+
+sub _dump {
+  my($self)=@_;
+  join("\n", map {
+    "$_: ". $self->getfield($_). "|"
+  } (fields($self->table)) );
+}
+
+sub DESTROY { return; }
+
+#sub DESTROY {
+#  my $self = shift;
+#  #use Carp qw(cluck);
+#  #cluck "DESTROYING $self";
+#  warn "DESTROYING $self";
+#}
+
+#sub is_tainted {
+#             return ! eval { join('',@_), kill 0; 1; };
+#         }
+
+=back
+
+=head1 BUGS
+
+This module should probably be renamed, since much of the functionality is
+of general use.  It is not completely unlike Adapter::DBI (see below).
+
+Exported qsearch and qsearchs should be deprecated in favor of method calls
+(against an FS::Record object like the old search and searchs that qsearch
+and qsearchs were on top of.)
+
+The whole fields / hfields mess should be removed.
+
+The various WHERE clauses should be subroutined.
+
+table string should be deprecated in favor of DBIx::DBSchema::Table.
+
+No doubt we could benefit from a Tied hash.  Documenting how exists / defined
+true maps to the database (and WHERE clauses) would also help.
+
+The ut_ methods should ask the dbdef for a default length.
+
+ut_sqltype (like ut_varchar) should all be defined
+
+A fallback check method should be provided which uses the dbdef.
+
+The ut_money method assumes money has two decimal digits.
+
+The Pg money kludge in the new method only strips `$'.
+
+The ut_phonen method only checks US-style phone numbers.
+
+The _quote function should probably use ut_float instead of a regex.
+
+All the subroutines probably should be methods, here or elsewhere.
+
+Probably should borrow/use some dbdef methods where appropriate (like sub
+fields)
+
+As of 1.14, DBI fetchall_hashref( {} ) doesn't set fetchrow_hashref NAME_lc,
+or allow it to be set.  Working around it is ugly any way around - DBI should
+be fixed.  (only affects RDBMS which return uppercase column names)
+
+ut_zip should take an optional country like ut_phone.
+
+=head1 SEE ALSO
+
+L<DBIx::DBSchema>, L<FS::UID>, L<DBI>
+
+Adapter::DBI from Ch. 11 of Advanced Perl Programming by Sriram Srinivasan.
+
+=cut
+
+1;
+
diff --git a/FS/FS/SearchCache.pm b/FS/FS/SearchCache.pm
new file mode 100644 (file)
index 0000000..4218acf
--- /dev/null
@@ -0,0 +1,96 @@
+package FS::SearchCache;
+
+use strict;
+use vars qw($DEBUG);
+#use Carp qw(carp cluck croak confess);
+
+$DEBUG = 0;
+
+=head1 NAME
+
+FS::SearchCache - cache
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=over 4
+
+=item new
+
+=cut
+
+sub new { 
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my( $table, $key ) = @_;
+  warn "table $table\n" if $DEBUG > 1;
+  warn "key $key\n" if $DEBUG > 1;
+  my $self = { 'table' => $table,
+               'key'   => $key,
+               'cache' => {},
+               'subcache' => {},
+             };
+  bless ($self, $class);
+
+  $self;
+}
+
+=item table
+
+=cut
+
+sub table { my $self = shift; $self->{table}; }
+
+=item key
+
+=cut
+
+sub key { my $self = shift; $self->{key}; }
+
+=item cache
+
+=cut
+
+sub cache { my $self = shift; $self->{cache}; }
+
+=item subcache
+
+=cut
+
+sub subcache {
+  my $self = shift;
+  my $col = shift;
+  my $table = shift;
+  my $keyval = shift;
+  if ( exists $self->{subcache}->{$col}->{$keyval} ) {
+    warn "returning existing subcache for $keyval ($col)".
+         "$self->{subcache}->{$col}->{$keyval}\n" if $DEBUG;
+    return $self->{subcache}->{$col}->{$keyval};
+  } else {
+    #my $tablekey = @_ ? shift : $col;
+    my $tablekey = $col;
+    my $subcache = ref($self)->new( $table, $tablekey );
+    $self->{subcache}->{$col}->{$keyval} = $subcache;
+    warn "creating new subcache $table $tablekey: $subcache\n" if $DEBUG;
+    $subcache;
+  }
+}
+
+=back
+
+=head1 BUGS
+
+Dismal documentation.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_main>
+
+=cut
+
+1;
+
+
diff --git a/FS/FS/UI/Base.pm b/FS/FS/UI/Base.pm
new file mode 100644 (file)
index 0000000..bbeb9e1
--- /dev/null
@@ -0,0 +1,194 @@
+package FS::UI::Base;
+
+use strict;
+use vars qw ( @ISA );
+use FS::Record qw( fields qsearch );
+
+@ISA = ( $FS::UI::Base::_lock );
+
+=head1 NAME
+
+FS::UI::Base - Base class for all user-interface objects
+
+=head1 SYNOPSIS
+
+  use FS::UI::SomeInterface;
+  use FS::UI::some_table;
+
+  $interface = new FS::UI::some_table;
+
+  $error = $interface->browse;
+  $error = $interface->search;
+  $error = $interface->view;
+  $error = $interface->edit;
+  $error = $interface->process;
+
+=head1 DESCRIPTION
+
+An FS::UI::Base object represents a user interface object.  FS::UI::Base
+is intended as a base class for table-specfic classes to inherit from, i.e.
+FS::UI::cust_main.  The simplest case, which will provide a default UI for your
+new table, is as follows:
+
+  package FS::UI::table_name;
+  use vars qw ( @ISA );
+  use FS::UI::Base;
+  @ISA = qw( FS::UI::Base );
+  sub db_table { 'table_name'; }
+
+Currently available interfaces are:
+  FS::UI::Gtk, an X-Windows UI implemented using the Gtk+ toolkit
+  FS::UI::CGI, a web interface implemented using CGI.pm, etc.
+
+=head1 METHODS
+
+=over 4
+
+=item new
+
+=cut
+
+=item browse
+
+=cut
+
+sub browse {
+  my $self = shift;
+
+  my @fields = $self->list_fields;
+
+  #begin browse-specific stuff
+
+  $self->title( "Browse ". $self->db_names ) unless $self->title;
+  my @records = qsearch ( $self->db_table, {} );
+
+  #end browse-specific stuff
+
+  $self->addwidget ( new FS::UI::_Text ( $self->db_description ) );
+
+  my @header = $self->list_header;
+  my @headerspan = $self->list_headerspan;
+  my %callback = $self->db_callback;
+
+  my $columns;
+
+  my $table = new FS::UI::_Tableborder (
+    'rows' => 1 + scalar(@records),
+    'columns' => $columns || scalar(@fields),
+  );
+
+  my $c = 0;
+  foreach my $header ( @header ) {
+    my $headerspan = shift(@headerspan) || 1;
+    $table->attach(
+      0, $c, new FS::UI::_Text ( $header ), 1, $headerspan
+    );
+    $c += $headerspan;
+  }
+
+  my $r = 1;
+  
+  foreach my $record ( @records ) {
+    $c = 0;
+    foreach my $field ( @fields ) {
+      my $value = $record->getfield($field);
+      my $widget;
+      if ( $callback{$field} ) {
+        $widget = &{ $callback{$field} }( $value, $record );
+      } else {
+        $widget = new FS::UI::_Text ( $value );
+      }
+      $table->attach( $r, $c++, $widget, 1, 1 );
+    }
+    $r++;
+  }
+
+  $self->addwidget( $table );
+
+  $self->activate;
+
+}
+
+=item title
+
+=cut
+
+sub title {
+  my $self = shift;
+  my $value = shift;
+  if ( defined($value) ) {
+    $self->{'title'} = $value;
+  } else {
+    $self->{'title'};
+  }
+}
+
+=item addwidget
+
+=cut
+
+sub addwidget {
+  my $self = shift;
+  my $widget = shift;
+  push @{ $self->{'Widgets'} }, $widget;
+}
+
+#fallback methods
+
+sub db_description {}
+
+sub db_name {}
+
+sub db_names {
+  my $self = shift;
+  $self->db_name. 's';
+}
+
+sub list_fields {
+  my $self = shift;
+  fields( $self->db_table );
+}
+
+sub list_header {
+  my $self = shift;
+  $self->list_fields
+}
+
+sub list_headerspan {
+  my $self = shift;
+  map 1, $self->list_header;
+}
+
+sub db_callback {}
+
+=back
+
+=head1 VERSION
+
+$Id: Base.pm,v 1.1 1999-08-04 09:03:53 ivan Exp $
+
+=head1 BUGS
+
+This documentation is incomplete.
+
+There should be some sort of per-(freeside)-user preferences and the ability
+for specific FS::UI:: modules to put their own values there as well.
+
+=head1 SEE ALSO
+
+L<FS::UI::Gtk>, L<FS::UI::CGI>
+
+=head1 HISTORY
+
+$Log: Base.pm,v $
+Revision 1.1  1999-08-04 09:03:53  ivan
+initial checkin of module files for proper perl installation
+
+Revision 1.1  1999/01/20 09:30:36  ivan
+skeletal cross-UI UI code.
+
+
+=cut
+
+1;
+
diff --git a/FS/FS/UI/CGI.pm b/FS/FS/UI/CGI.pm
new file mode 100644 (file)
index 0000000..ae87d13
--- /dev/null
@@ -0,0 +1,239 @@
+package FS::UI::CGI;
+
+use strict;
+use CGI;
+#use CGI::Switch;  #when FS::UID user and preference callback stuff is fixed
+use CGI::Carp qw(fatalsToBrowser);
+use HTML::Table;
+use FS::UID qw(adminsuidsetup);
+#use FS::Record qw( qsearch fields );
+
+die "Can't initialize CGI interface; $FS::UI::Base::_lock used"
+  if $FS::UI::Base::_lock;
+$FS::UI::Base::_lock = "FS::UI::CGI";
+
+=head1 NAME
+
+FS::UI::CGI - Base class for CGI user-interface objects
+
+=head1 SYNOPSIS
+
+  use FS::UI::CGI;
+  use FS::UI::some_table;
+
+  $interface = new FS::UI::some_table;
+
+  $error = $interface->browse;
+  $error = $interface->search;
+  $error = $interface->view;
+  $error = $interface->edit;
+  $error = $interface->process;
+
+=head1 DESCRIPTION
+
+An FS::UI::CGI object represents a CGI interface object.
+
+=head1 METHODS
+
+=over 4
+
+=item new
+
+=cut
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = { @_ };
+
+  $self->{'_cgi'} = new CGI;
+  $self->{'_user'} = $self->{'_cgi'}->remote_user;
+  $self->{'_dbh'} = FS::UID::adminsuidsetup $self->{'_user'};
+
+  bless ( $self, $class);
+}
+
+sub activate {
+  my $self = shift;
+  print $self->_header,
+        join ( "<BR>", map $_->sprint, @{ $self->{'Widgets'} } ),
+        $self->_footer,
+  ;
+}
+
+=item _header
+
+=cut
+
+sub _header {
+  my $self = shift;
+  my $cgi = $self->{'_cgi'};
+
+  $cgi->header( '-expires' => 'now' ), '<HTML>', 
+    '<HEAD><TITLE>', $self->title, '</TITLE></HEAD>',
+    '<BODY BGCOLOR="#ffffff">',
+    '<FONT COLOR="#ff0000" SIZE=7>', $self->title, '</FONT><BR><BR>',
+  ;
+}
+
+=item _footer
+
+=cut
+
+sub _footer {
+  "</BODY></HTML>";
+}
+
+=item interface
+
+Returns the string `CGI'.  Useful for the author of a table-specific UI class
+to conditionally specify certain behaviour.
+
+=cut
+
+sub interface { 'CGI'; }
+
+=back
+
+=cut
+
+package FS::UI::_Widget;
+
+use vars qw( $AUTOLOAD );
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = { @_ };
+  bless ( $self, $class );
+}
+
+sub AUTOLOAD {
+  my $self = shift;
+  my $value = shift;
+  my($field)=$AUTOLOAD;
+  $field =~ s/.*://;
+  if ( defined($value) ) {
+    $self->{$field} = $value;
+  } else {
+    $self->{$field};
+  }    
+}
+
+package FS::UI::_Text;
+
+use vars qw ( @ISA );
+
+@ISA = qw ( FS::UI::_Widget);
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = {};
+  $self->{'_text'} = shift;
+  bless ( $self, $class );
+}
+
+sub sprint {
+  my $self = shift;
+  $self->{'_text'};
+}
+
+package FS::UI::_Link;
+
+use vars qw ( @ISA $BASE_URL );
+
+@ISA = qw ( FS::UI::_Widget);
+$BASE_URL = "http://rootwood.sisd.com/freeside";
+
+sub sprint {
+  my $self = shift;
+  my $table = $self->{'table'};
+  my $method = $self->{'method'};
+
+  # i will be cleaned up when we're done moving from the old webinterface!
+  my @arg = @{$self->{'arg'}};
+  my $yuck = join( "&", @arg);
+  qq(<A HREF="$BASE_URL/$method/$table.cgi?$yuck">). $self->{'text'}. "<\A>";
+}
+
+package FS::UI::_Table;
+
+use vars qw ( @ISA );
+
+@ISA = qw ( FS::UI::_Widget);
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = $class eq $proto ? { @_ } : $proto;
+  bless ( $self, $class );
+  $self->{'_table'} = new HTML::Table ( $self->rows, $self->columns );
+  $self;
+}
+
+sub attach {
+  my $self = shift;
+  my ( $row, $column, $widget, $rowspan, $colspan ) = @_;
+  $self->{"_table"}->setCell( $row+1, $column+1, $widget->sprint );
+  $self->{"_table"}->setCellRowSpan( $row+1, $column+1, $rowspan ) if $rowspan;
+  $self->{"_table"}->setCellColSpan( $row+1, $column+1, $colspan ) if $colspan;
+}
+
+sub sprint {
+  my $self = shift;
+  $self->{'_table'}->getTable;
+}
+
+package FS::UI::_Tableborder;
+
+use vars qw ( @ISA );
+
+@ISA = qw ( FS::UI::_Table );
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = $class eq $proto ? { @_ } : $proto;
+  bless ( $self, $class );
+  $self->SUPER::new(@_);
+  $self->{'_table'}->setBorder;
+  $self;
+}
+
+=head1 VERSION
+
+$Id: CGI.pm,v 1.1 1999-08-04 09:03:53 ivan Exp $
+
+=head1 BUGS
+
+This documentation is incomplete.
+
+In _Tableborder, headers should be links that sort on their fields.
+
+_Link uses a constant $BASE_URL
+
+_Link passes the arguments as a manually-constructed GET string instead
+of POSTing, for compatability while the web interface is upgraded.  Once
+this is done it should pass arguements properly (i.e. as a POST, 8-bit clean)
+
+Still some small bits of widget code same as FS::UI::Gtk.
+
+=head1 SEE ALSO
+
+L<FS::UI::Base>
+
+=head1 HISTORY
+
+$Log: CGI.pm,v $
+Revision 1.1  1999-08-04 09:03:53  ivan
+initial checkin of module files for proper perl installation
+
+Revision 1.1  1999/01/20 09:30:36  ivan
+skeletal cross-UI UI code.
+
+
+=cut
+
+1;
+
diff --git a/FS/FS/UI/Gtk.pm b/FS/FS/UI/Gtk.pm
new file mode 100644 (file)
index 0000000..507a293
--- /dev/null
@@ -0,0 +1,224 @@
+package FS::UI::Gtk;
+
+use strict;
+use Gtk;
+use FS::UID qw(adminsuidsetup);
+
+die "Can't initialize Gtk interface; $FS::UI::Base::_lock used"
+  if $FS::UI::Base::_lock;
+$FS::UI::Base::_lock = "FS::UI::Gtk";
+
+=head1 NAME
+
+FS::UI::Gtk - Base class for Gtk user-interface objects
+
+=head1 SYNOPSIS
+
+  use FS::UI::Gtk;
+  use FS::UI::some_table;
+
+  $interface = new FS::UI::some_table;
+
+  $error = $interface->browse;
+  $error = $interface->search;
+  $error = $interface->view;
+  $error = $interface->edit;
+  $error = $interface->process;
+
+=head1 DESCRIPTION
+
+An FS::UI::Gtk object represents a Gtk user interface object.
+
+=head1 METHODS
+
+=over 4
+
+=item new
+
+=cut
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = { @_ };
+
+  bless ( $self, $class );
+
+  $self->{'_user'} = 'ivan'; #Pop up login window?
+  $self->{'_dbh'} = FS::UID::adminsuidsetup $self->{'_user'};
+
+
+
+  $self;
+}
+
+sub activate {
+  my $self = shift;
+
+  my $vbox = new Gtk::VBox ( 0, 4 );
+
+  foreach my $widget ( @{ $self->{'Widgets'} } ) {
+    $widget->_gtk->show;
+    $vbox->pack_start ( $widget->_gtk, 1, 1, 4 );
+  }
+  $vbox->show;
+
+  my $window = new Gtk::Window "toplevel";
+  $self->{'_gtk'} = $window;
+  $window->set_title( $self->title );
+  $window->add ( $vbox );
+  $window->show;
+  main Gtk;
+}
+
+=item interface
+
+Returns the string `Gtk'.  Useful for the author of a table-specific UI class
+to conditionally specify certain behaviour.
+
+=cut 
+
+sub interface { 'Gtk'; }
+
+=back
+
+=cut
+
+package FS::UI::_Widget;
+
+use vars qw( $AUTOLOAD );
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = { @_ };
+  bless ( $self, $class );
+}
+
+sub _gtk {
+  my $self = shift;
+  $self->{'_gtk'};
+}
+
+sub AUTOLOAD {
+  my $self = shift;
+  my $value = shift;
+  my($field)=$AUTOLOAD;
+  $field =~ s/.*://;
+  if ( defined($value) ) {
+    $self->{$field} = $value;
+  } else {
+    $self->{$field};
+  }    
+}
+
+package FS::UI::_Text;
+
+use vars qw ( @ISA );
+
+@ISA = qw ( FS::UI::_Widget );
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = {};
+  $self->{'_gtk'} = new Gtk::Label ( shift );
+  bless ( $self, $class );
+}
+
+package FS::UI::_Link;
+
+use vars qw ( @ISA );
+
+@ISA = qw ( FS::UI::_Widget );
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = { @_ };
+  $self->{'_gtk'} = new_with_label Gtk::Button ( $self->{'text'} );
+  $self->{'_gtk'}->signal_connect( 'clicked', sub {
+      print "STUB: (Gtk) FS::UI::_Link";
+    }, "hi", "there" );
+  bless ( $self, $class );
+}
+
+
+package FS::UI::_Table;
+
+use vars qw ( @ISA );
+
+@ISA = qw ( FS::UI::_Widget );
+
+sub new {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = { @_ };
+  bless ( $self, $class );
+
+  $self->{'_gtk'} = new Gtk::Table (
+    $self->rows,
+    $self->columns,
+    0, #homogeneous
+  );
+
+  $self;
+}
+
+sub attach {
+  my $self = shift;
+  my ( $row, $column, $widget, $rowspan, $colspan ) = @_;
+  $rowspan ||= 1;
+  $colspan ||= 1;
+  $self->_gtk->attach_defaults(
+    $widget->_gtk,
+    $column,
+    $column + $colspan,
+    $row,
+    $row + $rowspan,
+  );
+  $widget->_gtk->show;
+}
+
+package FS::UI::_Tableborder;
+
+use vars qw ( @ISA );
+
+@ISA = qw ( FS::UI::_Table );
+
+=head1 VERSION
+
+$Id: Gtk.pm,v 1.1 1999-08-04 09:03:53 ivan Exp $
+
+=head1 BUGS
+
+This documentation is incomplete.
+
+_Tableborder is just a _Table now.  _Tableborders should scroll (but not the
+headers) and need and need more decoration. (data in white section ala gtksql
+and sliding field widths) headers should be buttons that callback to sort on
+their fields.
+
+There should be a persistant, per-(freeside)-user store for window positions
+and sizes and sort fields etc (see L<FS::UI::CGI/BUGS>.
+
+Still some small bits of widget code same as FS::UI::CGI.
+
+=head1 SEE ALSO
+
+L<FS::UI::Base>
+
+=head1 HISTORY
+
+$Log: Gtk.pm,v $
+Revision 1.1  1999-08-04 09:03:53  ivan
+initial checkin of module files for proper perl installation
+
+Revision 1.1  1999/01/20 09:30:36  ivan
+skeletal cross-UI UI code.
+
+
+=cut
+
+1;
+
diff --git a/FS/FS/UI/agent.pm b/FS/FS/UI/agent.pm
new file mode 100644 (file)
index 0000000..ce9744a
--- /dev/null
@@ -0,0 +1,62 @@
+package FS::UI::agent;
+
+use strict;
+use vars qw ( @ISA );
+use FS::UI::Base;
+use FS::Record qw( qsearchs );
+use FS::agent;
+use FS::agent_type;
+
+@ISA = qw ( FS::UI::Base );
+
+sub db_table { 'agent' };
+
+sub db_name { 'Agent' };
+
+sub db_description { <<END;
+Agents are resellers of your service. Agents may be limited to a subset of your
+full offerings (via their type).
+END
+}
+
+sub list_fields {
+  'agentnum',
+  'typenum',
+#  'freq',
+#  'prog',
+; }
+
+sub list_header {
+  'Agent',
+  'Type',
+#  'Freq (n/a)',
+#  'Prog (n/a)',
+; }
+
+sub db_callback { 
+  'agentnum' =>
+    sub {
+      my ( $agentnum, $record ) = @_;
+      my $agent = $record->agent;
+      new FS::UI::_Link (
+        'table'  => 'agent',
+        'method' => 'edit',
+        'arg'    => [ $agentnum ],
+        'text'   => "$agentnum: $agent",
+      );
+    },
+  'typenum' =>
+    sub {
+      my $typenum = shift;
+      my $agent_type = qsearchs( 'agent_type', { 'typenum' => $typenum } );
+      my $atype = $agent_type->atype;
+      new FS::UI::_Link (
+        'table'  => 'agent_type',
+        'method' => 'edit',
+        'arg'    => [ $typenum ],
+        'text'   => "$typenum: $atype"
+      );
+    },
+}
+
+1;
diff --git a/FS/FS/UID.pm b/FS/FS/UID.pm
new file mode 100644 (file)
index 0000000..f670051
--- /dev/null
@@ -0,0 +1,316 @@
+package FS::UID;
+
+use strict;
+use vars qw(
+  @ISA @EXPORT_OK $cgi $dbh $freeside_uid $user 
+  $conf_dir $secrets $datasrc $db_user $db_pass %callback @callback
+  $driver_name $AutoCommit
+);
+use subs qw(
+  getsecrets cgisetotaker
+);
+use Exporter;
+use Carp qw(carp croak cluck);
+use DBI;
+use FS::Conf;
+
+@ISA = qw(Exporter);
+@EXPORT_OK = qw(checkeuid checkruid cgisuidsetup adminsuidsetup forksuidsetup
+                getotaker dbh datasrc getsecrets driver_name );
+
+$freeside_uid = scalar(getpwnam('freeside'));
+
+$conf_dir = "/usr/local/etc/freeside/";
+
+$AutoCommit = 1; #ours, not DBI
+
+=head1 NAME
+
+FS::UID - Subroutines for database login and assorted other stuff
+
+=head1 SYNOPSIS
+
+  use FS::UID qw(adminsuidsetup cgisuidsetup dbh datasrc getotaker
+  checkeuid checkruid);
+
+  adminsuidsetup $user;
+
+  $cgi = new CGI;
+  $dbh = cgisuidsetup($cgi);
+
+  $dbh = dbh;
+
+  $datasrc = datasrc;
+
+  $driver_name = driver_name;
+
+=head1 DESCRIPTION
+
+Provides a hodgepodge of subroutines. 
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item adminsuidsetup USER
+
+Sets the user to USER (see config.html from the base documentation).
+Cleans the environment.
+Make sure the script is running as freeside, or setuid freeside.
+Opens a connection to the database.
+Swaps real and effective UIDs.
+Runs any defined callbacks (see below).
+Returns the DBI database handle (usually you don't need this).
+
+=cut
+
+sub adminsuidsetup {
+  $dbh->disconnect if $dbh;
+  &forksuidsetup(@_);
+}
+
+sub forksuidsetup {
+  $user = shift;
+  croak "fatal: adminsuidsetup called without arguements" unless $user;
+
+  $user =~ /^([\w\-\.]+)$/ or croak "fatal: illegal user $user";
+  $user = $1;
+
+  $ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
+  $ENV{'SHELL'} = '/bin/sh';
+  $ENV{'IFS'} = " \t\n";
+  $ENV{'CDPATH'} = '';
+  $ENV{'ENV'} = '';
+  $ENV{'BASH_ENV'} = '';
+
+  croak "Not running uid freeside!" unless checkeuid();
+  getsecrets;
+  $dbh = DBI->connect($datasrc,$db_user,$db_pass, {
+                          'AutoCommit' => 0,
+                          #'ChopBlanks' => 1,
+  } ) or die "DBI->connect error: $DBI::errstr\n";
+
+  foreach ( keys %callback ) {
+    &{$callback{$_}};
+    # breaks multi-database installs # delete $callback{$_}; #run once
+  }
+
+  &{$_} foreach @callback;
+
+  $dbh;
+}
+
+=item install_callback
+
+A package can install a callback to be run in adminsuidsetup by passing
+a coderef to the FS::UID->install_callback class method.  If adminsuidsetup has
+run already, the callback will also be run immediately.
+
+    $coderef = sub { warn "Hi, I'm returning your call!" };
+    FS::UID->install_callback($coderef);
+
+    install_callback FS::UID sub { 
+      warn "Hi, I'm returning your call!"
+    };
+
+=cut
+
+sub install_callback {
+  my $class = shift;
+  my $callback = shift;
+  push @callback, $callback;
+  &{$callback} if $dbh;
+}
+
+=item cgisuidsetup CGI_object
+
+Takes a single argument, which is a CGI (see L<CGI>) or Apache (see L<Apache>)
+object (CGI::Base is depriciated).  Runs cgisetotaker and then adminsuidsetup.
+
+=cut
+
+sub cgisuidsetup {
+  $cgi=shift;
+  if ( $cgi->isa('CGI::Base') ) {
+    carp "Use of CGI::Base is depriciated";
+  } elsif ( $cgi->isa('Apache') ) {
+
+  } elsif ( ! $cgi->isa('CGI') ) {
+    croak "fatal: unrecognized object $cgi";
+  }
+  cgisetotaker; 
+  adminsuidsetup($user);
+}
+
+=item cgi
+
+Returns the CGI (see L<CGI>) object.
+
+=cut
+
+sub cgi {
+  carp "warning: \$FS::UID::cgi isa Apache" if $cgi->isa('Apache');
+  $cgi;
+}
+
+=item dbh
+
+Returns the DBI database handle.
+
+=cut
+
+sub dbh {
+  $dbh;
+}
+
+=item datasrc
+
+Returns the DBI data source.
+
+=cut
+
+sub datasrc {
+  $datasrc;
+}
+
+=item driver_name
+
+Returns just the driver name portion of the DBI data source.
+
+=cut
+
+sub driver_name {
+  return $driver_name if defined $driver_name;
+  $driver_name = ( split(':', $datasrc) )[1];
+}
+
+sub suidsetup {
+  croak "suidsetup depriciated";
+}
+
+=item getotaker
+
+Returns the current Freeside user.
+
+=cut
+
+sub getotaker {
+  $user;
+}
+
+=item cgisetotaker
+
+Sets and returns the CGI REMOTE_USER.  $cgi should be defined as a CGI.pm
+object (see L<CGI>) or an Apache object (see L<Apache>).  Support for CGI::Base
+and derived classes is depriciated.
+
+=cut
+
+sub cgisetotaker {
+  if ( $cgi && $cgi->isa('CGI::Base') && defined $cgi->var('REMOTE_USER')) {
+    carp "Use of CGI::Base is depriciated";
+    $user = lc ( $cgi->var('REMOTE_USER') );
+  } elsif ( $cgi && $cgi->isa('CGI') && defined $cgi->remote_user ) {
+    $user = lc ( $cgi->remote_user );
+  } elsif ( $cgi && $cgi->isa('Apache') ) {
+    $user = lc ( $cgi->connection->user );
+  } else {
+    die "fatal: Can't get REMOTE_USER! for cgi $cgi - you need to setup ".
+        "Apache user authentication as documented in httemplate/docs/install.html";
+  }
+  $user;
+}
+
+=item checkeuid
+
+Returns true if effective UID is that of the freeside user.
+
+=cut
+
+sub checkeuid {
+  ( $> == $freeside_uid );
+}
+
+=item checkruid
+
+Returns true if the real UID is that of the freeside user.
+
+=cut
+
+sub checkruid {
+  ( $< == $freeside_uid );
+}
+
+=item getsecrets [ USER ]
+
+Sets the user to USER, if supplied.
+Sets and returns the DBI datasource, username and password for this user from
+the `/usr/local/etc/freeside/mapsecrets' file.
+
+=cut
+
+sub getsecrets {
+  my($setuser) = shift;
+  $user = $setuser if $setuser;
+  die "No user!" unless $user;
+  my($conf) = new FS::Conf $conf_dir;
+  my($line) = grep /^\s*$user\s/, $conf->config('mapsecrets');
+  die "User $user not found in mapsecrets!" unless $line;
+  $line =~ /^\s*$user\s+(.*)$/;
+  $secrets = $1;
+  die "Illegal mapsecrets line for user?!" unless $secrets;
+  ($datasrc, $db_user, $db_pass) = $conf->config($secrets)
+    or die "Can't get secrets: $!";
+  $FS::Conf::default_dir = $conf_dir. "/conf.$datasrc";
+  undef $driver_name;
+  ($datasrc, $db_user, $db_pass);
+}
+
+=back
+
+=head1 CALLBACKS
+
+Warning: this interface is (still) likely to change in future releases.
+
+New (experimental) callback interface:
+
+A package can install a callback to be run in adminsuidsetup by passing
+a coderef to the FS::UID->install_callback class method.  If adminsuidsetup has
+run already, the callback will also be run immediately.
+
+    $coderef = sub { warn "Hi, I'm returning your call!" };
+    FS::UID->install_callback($coderef);
+
+    install_callback FS::UID sub { 
+      warn "Hi, I'm returning your call!"
+    };
+
+Old (deprecated) callback interface:
+
+A package can install a callback to be run in adminsuidsetup by putting a
+coderef into the hash %FS::UID::callback :
+
+    $coderef = sub { warn "Hi, I'm returning your call!" };
+    $FS::UID::callback{'Package::Name'} = $coderef;
+
+=head1 BUGS
+
+Too many package-global variables.
+
+Not OO.
+
+No capabilities yet.  When mod_perl and Authen::DBI are implemented, 
+cgisuidsetup will go away as well.
+
+Goes through contortions to support non-OO syntax with multiple datasrc's.
+
+Callbacks are (still) inelegant.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<CGI>, L<DBI>, config.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm
new file mode 100755 (executable)
index 0000000..c5ddca7
--- /dev/null
@@ -0,0 +1,330 @@
+package FS::addr_block;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs qsearch dbh );
+use FS::router;
+use FS::svc_broadband;
+use FS::Conf;
+use NetAddr::IP;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::addr_block - Object methods for addr_block records
+
+=head1 SYNOPSIS
+
+  use FS::addr_block;
+
+  $record = new FS::addr_block \%hash;
+  $record = new FS::addr_block { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::addr_block record describes an address block assigned for broadband 
+access.  FS::addr_block inherits from FS::Record.  The following fields are 
+currently supported:
+
+=over 4
+
+=item blocknum - primary key, used in FS::svc_broadband to associate 
+services to the block.
+
+=item routernum - the router (see FS::router) to which this 
+block is assigned.
+
+=item ip_gateway - the gateway address used by customers within this block.  
+
+=item ip_netmask - the netmask of the block, expressed as an integer.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'addr_block'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+sub delete {
+  my $self = shift;
+  return 'Block must be deallocated before deletion'
+    if $self->router;
+
+  $self->SUPER::delete;
+}
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_number('routernum')
+    || $self->ut_ip('ip_gateway')
+    || $self->ut_number('ip_netmask')
+  ;
+  return $error if $error;
+
+
+  # A routernum of 0 indicates an unassigned block and is allowed
+  return "Unknown routernum"
+    if ($self->routernum and not $self->router);
+
+  my $self_addr = $self->NetAddr;
+  return "Cannot parse address: ". $self->ip_gateway . '/' . $self->ip_netmask
+    unless $self_addr;
+
+  if (not $self->blocknum) {
+    my @block = grep {
+      my $block_addr = $_->NetAddr;
+      if($block_addr->contains($self_addr) 
+      or $self_addr->contains($block_addr)) { $_; };
+    } qsearch( 'addr_block', {});
+    foreach(@block) {
+      return "Block intersects existing block ".$_->ip_gateway."/".$_->ip_netmask;
+    }
+  }
+
+  '';
+}
+
+
+=item router
+
+Returns the FS::router object corresponding to this object.  If the 
+block is unassigned, returns undef.
+
+=cut
+
+sub router {
+  my $self = shift;
+  return qsearchs('router', { routernum => $self->routernum });
+}
+
+=item svc_broadband
+
+Returns a list of FS::svc_broadband objects associated
+with this object.
+
+=cut
+
+sub svc_broadband {
+  my $self = shift;
+  return qsearch('svc_broadband', { blocknum => $self->blocknum });
+}
+
+=item NetAddr
+
+Returns a NetAddr::IP object for this block's address and netmask.
+
+=cut
+
+sub NetAddr {
+  my $self = shift;
+
+  return new NetAddr::IP ($self->ip_gateway, $self->ip_netmask);
+}
+
+=item next_free_addr
+
+Returns a NetAddr::IP object corresponding to the first unassigned address 
+in the block (other than the network, broadcast, or gateway address).  If 
+there are no free addresses, returns false.
+
+=cut
+
+sub next_free_addr {
+  my $self = shift;
+
+  my $conf = new FS::Conf;
+  my @excludeaddr = $conf->config('exclude_ip_addr');
+  
+  my @used = (
+    map { $_->NetAddr->addr } 
+      ($self, 
+       qsearch('svc_broadband', { blocknum => $self->blocknum }) ),
+     @excludeaddr );
+
+  my @free = $self->NetAddr->hostenum;
+  while (my $ip = shift @free) {
+    if (not grep {$_ eq $ip->addr;} @used) { return $ip; };
+  }
+
+  '';
+
+}
+
+=item allocate
+
+Allocates this address block to a router.  Takes an FS::router object 
+as an argument.
+
+At present it's not possible to reallocate a block to a different router 
+except by deallocating it first, which requires that none of its addresses 
+be assigned.  This is probably as it should be.
+
+=cut
+
+sub allocate {
+  my ($self, $router) = @_;
+
+  return 'Block is already allocated'
+    if($self->router);
+
+  return 'Block must be allocated to a router'
+    unless(ref $router eq 'FS::router');
+
+  my @svc = $self->svc_broadband;
+  if (@svc) {
+    return 'Block has assigned addresses: '. join ', ', map {$_->ip_addr} @svc;
+  }
+
+  my $new = new FS::addr_block {$self->hash};
+  $new->routernum($router->routernum);
+  return $new->replace($self);
+
+}
+
+=item deallocate
+
+Deallocates the block (i.e. sets the routernum to 0).  If any addresses in the 
+block are assigned to services, it fails.
+
+=cut
+
+sub deallocate {
+  my $self = shift;
+
+  my @svc = $self->svc_broadband;
+  if (@svc) {
+    return 'Block has assigned addresses: '. join ', ', map {$_->ip_addr} @svc;
+  }
+
+  my $new = new FS::addr_block {$self->hash};
+  $new->routernum(0);
+  return $new->replace($self);
+}
+
+=item split_block
+
+Splits this address block into two equal blocks, occupying the same space as
+the original block.  The first of the two will also have the same blocknum.
+The gateway address of each block will be set to the first usable address, i.e.
+(network address)+1.  Since this method is designed for use on unallocated
+blocks, this is probably the correct behavior.
+
+(At present, splitting allocated blocks is disallowed.  Anyone who wants to
+implement this is reminded that each split costs three addresses, and any
+customers who were using these addresses will have to be moved; depending on
+how full the block was before being split, they might have to be moved to a
+different block.  Anyone who I<still> wants to implement it is asked to tie it
+to a configuration switch so that site admins can disallow it.)
+
+=cut
+
+sub split_block {
+
+  # We should consider using Attribute::Handlers/Aspect/Hook::LexWrap/
+  # something to atomicize functions, so that we can say 
+  #
+  # sub split_block : atomic {
+  # 
+  # instead of repeating all this AutoCommit verbage in every 
+  # sub that does more than one database operation.
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $self = shift;
+  my $error;
+
+  if ($self->router) {
+    return 'Block is already allocated';
+  }
+
+  #TODO: Smallest allowed block should be a config option.
+  if ($self->NetAddr->masklen() ge 30) {
+    return 'Cannot split blocks with a mask length >= 30';
+  }
+
+  my (@new, @ip);
+  $ip[0] = $self->NetAddr;
+  @ip = map {$_->first()} $ip[0]->split($self->ip_netmask + 1);
+
+  foreach (0,1) {
+    $new[$_] = new FS::addr_block {$self->hash};
+    $new[$_]->ip_gateway($ip[$_]->addr);
+    $new[$_]->ip_netmask($ip[$_]->masklen);
+  }
+
+  $new[1]->blocknum('');
+
+  $error = $new[0]->replace($self);
+  if ($error) {
+    $dbh->rollback;
+    return $error;
+  }
+
+  $error = $new[1]->insert;
+  if ($error) {
+    $dbh->rollback;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  return '';
+}
+
+=item merge
+
+To be implemented.
+
+=back
+
+=head1 BUGS
+
+Minimum block size should be a config option.  It's hardcoded at /30 right
+now because that's the smallest block that makes any sense at all.
+
+=cut
+
+1;
+
diff --git a/FS/FS/agent.pm b/FS/FS/agent.pm
new file mode 100644 (file)
index 0000000..f11a28d
--- /dev/null
@@ -0,0 +1,160 @@
+package FS::agent;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_main;
+use FS::agent_type;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::agent - Object methods for agent records
+
+=head1 SYNOPSIS
+
+  use FS::agent;
+
+  $record = new FS::agent \%hash;
+  $record = new FS::agent { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $agent_type = $record->agent_type;
+
+  $hashref = $record->pkgpart_hashref;
+  #may purchase $pkgpart if $hashref->{$pkgpart};
+
+=head1 DESCRIPTION
+
+An FS::agent object represents an agent.  Every customer has an agent.  Agents
+can be used to track things like resellers or salespeople.  FS::agent inherits
+from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item agentnum - primary key (assigned automatically for new agents)
+
+=item agent - Text name of this agent
+
+=item typenum - Agent type.  See L<FS::agent_type>
+
+=item prog - For future use.
+
+=item freq - For future use.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new agent.  To add the agent to the database, see L<"insert">.
+
+=cut
+
+sub table { 'agent'; }
+
+=item insert
+
+Adds this agent to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this agent from the database.  Only agents with no customers can be
+deleted.  If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub delete {
+  my $self = shift;
+
+  return "Can't delete an agent with customers!"
+    if qsearch( 'cust_main', { 'agentnum' => $self->agentnum } );
+
+  $self->SUPER::delete;
+}
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid agent.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('agentnum')
+      || $self->ut_text('agent')
+      || $self->ut_number('typenum')
+      || $self->ut_numbern('freq')
+      || $self->ut_textn('prog')
+  ;
+  return $error if $error;
+
+  return "Unknown typenum!"
+    unless $self->agent_type;
+
+  '';
+
+}
+
+=item agent_type
+
+Returns the FS::agent_type object (see L<FS::agent_type>) for this agent.
+
+=cut
+
+sub agent_type {
+  my $self = shift;
+  qsearchs( 'agent_type', { 'typenum' => $self->typenum } );
+}
+
+=item pkgpart_hashref
+
+Returns a hash reference.  The keys of the hash are pkgparts.  The value is
+true if this agent may purchase the specified package definition.  See
+L<FS::part_pkg>.
+
+=cut
+
+sub pkgpart_hashref {
+  my $self = shift;
+  $self->agent_type->pkgpart_hashref;
+}
+
+=back
+
+=head1 VERSION
+
+$Id: agent.pm,v 1.3 2002-03-24 18:23:47 ivan Exp $
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, L<FS::part_pkg>, 
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/agent_type.pm b/FS/FS/agent_type.pm
new file mode 100644 (file)
index 0000000..988533a
--- /dev/null
@@ -0,0 +1,165 @@
+package FS::agent_type;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch );
+use FS::agent;
+use FS::type_pkgs;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::agent_type - Object methods for agent_type records
+
+=head1 SYNOPSIS
+
+  use FS::agent_type;
+
+  $record = new FS::agent_type \%hash;
+  $record = new FS::agent_type { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $hashref = $record->pkgpart_hashref;
+  #may purchase $pkgpart if $hashref->{$pkgpart};
+
+  @type_pkgs = $record->type_pkgs;
+
+  @pkgparts = $record->pkgpart;
+
+=head1 DESCRIPTION
+
+An FS::agent_type object represents an agent type.  Every agent (see
+L<FS::agent>) has an agent type.  Agent types define which packages (see
+L<FS::part_pkg>) may be purchased by customers (see L<FS::cust_main>), via 
+FS::type_pkgs records (see L<FS::type_pkgs>).  FS::agent_type inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item typenum - primary key (assigned automatically for new agent types)
+
+=item atype - Text name of this agent type
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new agent type.  To add the agent type to the database, see
+L<"insert">.
+
+=cut
+
+sub table { 'agent_type'; }
+
+=item insert
+
+Adds this agent type to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this agent type from the database.  Only agent types with no agents
+can be deleted.  If there is an error, returns the error, otherwise returns
+false.
+
+=cut
+
+sub delete {
+  my $self = shift;
+
+  return "Can't delete an agent_type with agents!"
+    if qsearch( 'agent', { 'typenum' => $self->typenum } );
+
+  $self->SUPER::delete;
+}
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid agent type.  If there is an
+error, returns the error, otherwise returns false.  Called by the insert and
+replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  $self->ut_numbern('typenum')
+  or $self->ut_text('atype');
+
+}
+
+=item pkgpart_hashref
+
+Returns a hash reference.  The keys of the hash are pkgparts.  The value is
+true iff this agent may purchase the specified package definition.  See
+L<FS::part_pkg>.
+
+=cut
+
+sub pkgpart_hashref {
+  my $self = shift;
+  my %pkgpart;
+  #$pkgpart{$_}++ foreach $self->pkgpart;
+  # not compatible w/5.004_04 (fixed in 5.004_05)
+  foreach ( $self->pkgpart ) { $pkgpart{$_}++; }
+  \%pkgpart;
+}
+
+=item type_pkgs
+
+Returns all FS::type_pkgs objects (see L<FS::type_pkgs>) for this agent type.
+
+=cut
+
+sub type_pkgs {
+  my $self = shift;
+  qsearch('type_pkgs', { 'typenum' => $self->typenum } );
+}
+
+=item pkgpart
+
+Returns the pkgpart of all package definitions (see L<FS::part_pkg>) for this
+agent type.
+
+=cut
+
+sub pkgpart {
+  my $self = shift;
+  map $_->pkgpart, $self->type_pkgs;
+}
+
+=back
+
+=head1 VERSION
+
+$Id: agent_type.pm,v 1.1 1999-08-04 09:03:53 ivan Exp $
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::agent>, L<FS::type_pkgs>, L<FS::cust_main>,
+L<FS::part_pkg>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
new file mode 100644 (file)
index 0000000..a22f44b
--- /dev/null
@@ -0,0 +1,902 @@
+package FS::cust_bill;
+
+use strict;
+use vars qw( @ISA $conf $money_char );
+use vars qw( $invoice_lines @buf ); #yuck
+use Date::Format;
+use Text::Template;
+use FS::UID qw( datasrc );
+use FS::Record qw( qsearch qsearchs );
+use FS::Misc qw( send_email );
+use FS::cust_main;
+use FS::cust_bill_pkg;
+use FS::cust_credit;
+use FS::cust_pay;
+use FS::cust_pkg;
+use FS::cust_credit_bill;
+use FS::cust_pay_batch;
+use FS::cust_bill_event;
+
+@ISA = qw( FS::Record );
+
+#ask FS::UID to run this stuff for us later
+FS::UID->install_callback( sub { 
+  $conf = new FS::Conf;
+  $money_char = $conf->config('money_char') || '$';  
+} );
+
+=head1 NAME
+
+FS::cust_bill - Object methods for cust_bill records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill;
+
+  $record = new FS::cust_bill \%hash;
+  $record = new FS::cust_bill { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
+
+  @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
+
+  ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
+
+  @cust_pay_objects = $cust_bill->cust_pay;
+
+  $tax_amount = $record->tax;
+
+  @lines = $cust_bill->print_text;
+  @lines = $cust_bill->print_text $time;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill object represents an invoice; a declaration that a customer
+owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
+(see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
+following fields are currently supported:
+
+=over 4
+
+=item invnum - primary key (assigned automatically for new invoices)
+
+=item custnum - customer (see L<FS::cust_main>)
+
+=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=item charged - amount of this invoice
+
+=item printed - deprecated
+
+=item closed - books closed flag, empty or `Y'
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new invoice.  To add the invoice to the database, see L<"insert">.
+Invoices are normally created by calling the bill method of a customer object
+(see L<FS::cust_main>).
+
+=cut
+
+sub table { 'cust_bill'; }
+
+=item insert
+
+Adds this invoice to the database ("Posts" the invoice).  If there is an error,
+returns the error, otherwise returns false.
+
+=item delete
+
+Currently unimplemented.  I don't remove invoices because there would then be
+no record you ever posted this invoice (which is bad, no?)
+
+=cut
+
+sub delete {
+  my $self = shift;
+  return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
+  $self->SUPER::delete(@_);
+}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+Only printed may be changed.  printed is normally updated by calling the
+collect method of a customer object (see L<FS::cust_main>).
+
+=cut
+
+sub replace {
+  my( $new, $old ) = ( shift, shift );
+  return "Can't change custnum!" unless $old->custnum == $new->custnum;
+  #return "Can't change _date!" unless $old->_date eq $new->_date;
+  return "Can't change _date!" unless $old->_date == $new->_date;
+  return "Can't change charged!" unless $old->charged == $new->charged;
+
+  $new->SUPER::replace($old);
+}
+
+=item check
+
+Checks all fields to make sure this is a valid invoice.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('invnum')
+    || $self->ut_number('custnum')
+    || $self->ut_numbern('_date')
+    || $self->ut_money('charged')
+    || $self->ut_numbern('printed')
+    || $self->ut_enum('closed', [ '', 'Y' ])
+  ;
+  return $error if $error;
+
+  return "Unknown customer"
+    unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+
+  $self->_date(time) unless $self->_date;
+
+  $self->printed(0) if $self->printed eq '';
+
+  ''; #no error
+}
+
+=item previous
+
+Returns a list consisting of the total previous balance for this customer, 
+followed by the previous outstanding invoices (as FS::cust_bill objects also).
+
+=cut
+
+sub previous {
+  my $self = shift;
+  my $total = 0;
+  my @cust_bill = sort { $a->_date <=> $b->_date }
+    grep { $_->owed != 0 && $_->_date < $self->_date }
+      qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
+  ;
+  foreach ( @cust_bill ) { $total += $_->owed; }
+  $total, @cust_bill;
+}
+
+=item cust_bill_pkg
+
+Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
+
+=cut
+
+sub cust_bill_pkg {
+  my $self = shift;
+  qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
+}
+
+=item cust_bill_event
+
+Returns the completed invoice events (see L<FS::cust_bill_event>) for this
+invoice.
+
+=cut
+
+sub cust_bill_event {
+  my $self = shift;
+  qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
+}
+
+
+=item cust_main
+
+Returns the customer (see L<FS::cust_main>) for this invoice.
+
+=cut
+
+sub cust_main {
+  my $self = shift;
+  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+}
+
+=item cust_credit
+
+Depreciated.  See the cust_credited method.
+
+ #Returns a list consisting of the total previous credited (see
+ #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
+ #outstanding credits (FS::cust_credit objects).
+
+=cut
+
+sub cust_credit {
+  use Carp;
+  croak "FS::cust_bill->cust_credit depreciated; see ".
+        "FS::cust_bill->cust_credit_bill";
+  #my $self = shift;
+  #my $total = 0;
+  #my @cust_credit = sort { $a->_date <=> $b->_date }
+  #  grep { $_->credited != 0 && $_->_date < $self->_date }
+  #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
+  #;
+  #foreach (@cust_credit) { $total += $_->credited; }
+  #$total, @cust_credit;
+}
+
+=item cust_pay
+
+Depreciated.  See the cust_bill_pay method.
+
+#Returns all payments (see L<FS::cust_pay>) for this invoice.
+
+=cut
+
+sub cust_pay {
+  use Carp;
+  croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
+  #my $self = shift;
+  #sort { $a->_date <=> $b->_date }
+  #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
+  #;
+}
+
+=item cust_bill_pay
+
+Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
+
+=cut
+
+sub cust_bill_pay {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
+}
+
+=item cust_credited
+
+Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
+
+=cut
+
+sub cust_credited {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
+  ;
+}
+
+=item tax
+
+Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
+
+=cut
+
+sub tax {
+  my $self = shift;
+  my $total = 0;
+  my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
+                                             'pkgnum' => 0 } );
+  foreach (@taxlines) { $total += $_->setup; }
+  $total;
+}
+
+=item owed
+
+Returns the amount owed (still outstanding) on this invoice, which is charged
+minus all payment applications (see L<FS::cust_bill_pay>) and credit
+applications (see L<FS::cust_credit_bill>).
+
+=cut
+
+sub owed {
+  my $self = shift;
+  my $balance = $self->charged;
+  $balance -= $_->amount foreach ( $self->cust_bill_pay );
+  $balance -= $_->amount foreach ( $self->cust_credited );
+  $balance = sprintf( "%.2f", $balance);
+  $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
+  $balance;
+}
+
+=item send
+
+Sends this invoice to the destinations configured for this customer: send
+emails or print.  See L<FS::cust_main_invoice>.
+
+=cut
+
+sub send {
+  my($self,$template) = @_;
+  my @print_text = $self->print_text('', $template);
+  my @invoicing_list = $self->cust_main->invoicing_list;
+
+  if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list  ) { #email
+
+    #better to notify this person than silence
+    @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list;
+
+    my $error = send_email(
+      'from'    => $conf->config('invoice_from'),
+      'to'      => [ grep { $_ ne 'POST' } @invoicing_list ],
+      'subject' => 'Invoice',
+      'body'    => \@print_text,
+    );
+    return "can't send invoice: $error" if $error;
+
+  }
+
+  if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
+    my $lpr = $conf->config('lpr');
+    open(LPR, "|$lpr")
+      or return "Can't open pipe to $lpr: $!";
+    print LPR @print_text;
+    close LPR
+      or return $! ? "Error closing $lpr: $!"
+                   : "Exit status $? from $lpr";
+  }
+
+  '';
+
+}
+
+=item send_csv OPTIONS
+
+Sends invoice as a CSV data-file to a remote host with the specified protocol.
+
+Options are:
+
+protocol - currently only "ftp"
+server
+username
+password
+dir
+
+The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
+and YYMMDDHHMMSS is a timestamp.
+
+The fields of the CSV file is as follows:
+
+record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
+
+=over 4
+
+=item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
+
+If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
+last five fields (B<pkg> through B<edate>) are irrelevant, and all other
+fields are filled in.
+
+If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
+first two fields (B<record_type> and B<invnum>) and the last five fields
+(B<pkg> through B<edate>) are filled in.
+
+=item invnum - invoice number
+
+=item custnum - customer number
+
+=item _date - invoice date
+
+=item charged - total invoice amount
+
+=item first - customer first name
+
+=item last - customer first name
+
+=item company - company name
+
+=item address1 - address line 1
+
+=item address2 - address line 1
+
+=item city
+
+=item state
+
+=item zip
+
+=item country
+
+=item pkg - line item description
+
+=item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
+
+=item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
+
+=item sdate - start date for recurring fee
+
+=item edate - end date for recurring fee
+
+=back
+
+=cut
+
+sub send_csv {
+  my($self, %opt) = @_;
+
+  #part one: create file
+
+  my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
+  mkdir $spooldir, 0700 unless -d $spooldir;
+
+  my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
+
+  open(CSV, ">$file") or die "can't open $file: $!";
+
+  eval "use Text::CSV_XS";
+  die $@ if $@;
+
+  my $csv = Text::CSV_XS->new({'always_quote'=>1});
+
+  my $cust_main = $self->cust_main;
+
+  $csv->combine(
+    'cust_bill',
+    $self->invnum,
+    $self->custnum,
+    time2str("%x", $self->_date),
+    sprintf("%.2f", $self->charged),
+    ( map { $cust_main->getfield($_) }
+        qw( first last company address1 address2 city state zip country ) ),
+    map { '' } (1..5),
+  ) or die "can't create csv";
+  print CSV $csv->string. "\n";
+
+  #new charges (false laziness w/print_text)
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+
+    my($pkg, $setup, $recur, $sdate, $edate);
+    if ( $cust_bill_pkg->pkgnum ) {
+    
+      ($pkg, $setup, $recur, $sdate, $edate) = (
+        $cust_bill_pkg->cust_pkg->part_pkg->pkg,
+        ( $cust_bill_pkg->setup != 0
+          ? sprintf("%.2f", $cust_bill_pkg->setup )
+          : '' ),
+        ( $cust_bill_pkg->recur != 0
+          ? sprintf("%.2f", $cust_bill_pkg->recur )
+          : '' ),
+        time2str("%x", $cust_bill_pkg->sdate),
+        time2str("%x", $cust_bill_pkg->edate),
+      );
+
+    } else { #pkgnum tax
+      next unless $cust_bill_pkg->setup != 0;
+      my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
+                       ? ( $cust_bill_pkg->itemdesc || 'Tax' )
+                       : 'Tax';
+      ($pkg, $setup, $recur, $sdate, $edate) =
+        ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
+    }
+
+    $csv->combine(
+      'cust_bill_pkg',
+      $self->invnum,
+      ( map { '' } (1..11) ),
+      ($pkg, $setup, $recur, $sdate, $edate)
+    ) or die "can't create csv";
+    print CSV $csv->string. "\n";
+
+  }
+
+  close CSV or die "can't close CSV: $!";
+
+  #part two: upload it
+
+  my $net;
+  if ( $opt{protocol} eq 'ftp' ) {
+    eval "use Net::FTP;";
+    die $@ if $@;
+    $net = Net::FTP->new($opt{server}) or die @$;
+  } else {
+    die "unknown protocol: $opt{protocol}";
+  }
+
+  $net->login( $opt{username}, $opt{password} )
+    or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
+
+  $net->binary or die "can't set binary mode";
+
+  $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
+
+  $net->put($file) or die "can't put $file: $!";
+
+  $net->quit;
+
+  unlink $file;
+
+}
+
+=item comp
+
+Pays this invoice with a compliemntary payment.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub comp {
+  my $self = shift;
+  my $cust_pay = new FS::cust_pay ( {
+    'invnum'   => $self->invnum,
+    'paid'     => $self->owed,
+    '_date'    => '',
+    'payby'    => 'COMP',
+    'payinfo'  => $self->cust_main->payinfo,
+    'paybatch' => '',
+  } );
+  $cust_pay->insert;
+}
+
+=item realtime_card
+
+Attempts to pay this invoice with a credit card payment via a
+Business::OnlinePayment realtime gateway.  See
+http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
+for supported processors.
+
+=cut
+
+sub realtime_card {
+  my $self = shift;
+  $self->realtime_bop( 'CC', @_ );
+}
+
+=item realtime_ach
+
+Attempts to pay this invoice with an electronic check (ACH) payment via a
+Business::OnlinePayment realtime gateway.  See
+http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
+for supported processors.
+
+=cut
+
+sub realtime_ach {
+  my $self = shift;
+  $self->realtime_bop( 'ECHECK', @_ );
+}
+
+=item realtime_lec
+
+Attempts to pay this invoice with phone bill (LEC) payment via a
+Business::OnlinePayment realtime gateway.  See
+http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
+for supported processors.
+
+=cut
+
+sub realtime_lec {
+  my $self = shift;
+  $self->realtime_bop( 'LEC', @_ );
+}
+
+sub realtime_bop {
+  my( $self, $method ) = @_;
+
+  my $cust_main = $self->cust_main;
+  my $amount = $self->owed;
+
+  my $description = 'Internet Services';
+  if ( $conf->exists('business-onlinepayment-description') ) {
+    my $dtempl = $conf->config('business-onlinepayment-description');
+
+    my $agent_obj = $cust_main->agent
+      or die "can't retreive agent for $cust_main (agentnum ".
+             $cust_main->agentnum. ")";
+    my $agent = $agent_obj->agent;
+    my $pkgs = join(', ',
+      map { $_->cust_pkg->part_pkg->pkg }
+        grep { $_->pkgnum } $self->cust_bill_pkg
+    );
+    $description = eval qq("$dtempl");
+  }
+
+  $cust_main->realtime_bop($method, $amount,
+    'description' => $description,
+    'invnum'      => $self->invnum,
+  );
+
+}
+
+=item batch_card
+
+Adds a payment for this invoice to the pending credit card batch (see
+L<FS::cust_pay_batch>).
+
+=cut
+
+sub batch_card {
+  my $self = shift;
+  my $cust_main = $self->cust_main;
+
+  my $cust_pay_batch = new FS::cust_pay_batch ( {
+    'invnum'   => $self->getfield('invnum'),
+    'custnum'  => $cust_main->getfield('custnum'),
+    'last'     => $cust_main->getfield('last'),
+    'first'    => $cust_main->getfield('first'),
+    'address1' => $cust_main->getfield('address1'),
+    'address2' => $cust_main->getfield('address2'),
+    'city'     => $cust_main->getfield('city'),
+    'state'    => $cust_main->getfield('state'),
+    'zip'      => $cust_main->getfield('zip'),
+    'country'  => $cust_main->getfield('country'),
+    'trancode' => 77,
+    'cardnum'  => $cust_main->getfield('payinfo'),
+    'exp'      => $cust_main->getfield('paydate'),
+    'payname'  => $cust_main->getfield('payname'),
+    'amount'   => $self->owed,
+  } );
+  my $error = $cust_pay_batch->insert;
+  die $error if $error;
+
+  '';
+}
+
+=item print_text [TIME];
+
+Returns an text invoice, as a list of lines.
+
+TIME an optional value used to control the printing of overdue messages.  The
+default is now.  It isn't the date of the invoice; that's the `_date' field.
+It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub print_text {
+
+  my( $self, $today, $template ) = @_;
+  $today ||= time;
+#  my $invnum = $self->invnum;
+  my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
+  $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
+    unless $cust_main->payname && $cust_main->payby ne 'CHEK';
+
+  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
+#  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
+  #my $balance_due = $self->owed + $pr_total - $cr_total;
+  my $balance_due = $self->owed + $pr_total;
+
+  #my @collect = ();
+  #my($description,$amount);
+  @buf = ();
+
+  #previous balance
+  foreach ( @pr_cust_bill ) {
+    push @buf, [
+      "Previous Balance, Invoice #". $_->invnum. 
+                 " (". time2str("%x",$_->_date). ")",
+      $money_char. sprintf("%10.2f",$_->owed)
+    ];
+  }
+  if (@pr_cust_bill) {
+    push @buf,['','-----------'];
+    push @buf,[ 'Total Previous Balance',
+                $money_char. sprintf("%10.2f",$pr_total ) ];
+    push @buf,['',''];
+  }
+
+  #new charges
+  foreach my $cust_bill_pkg (
+    ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
+    ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
+  ) {
+
+    if ( $cust_bill_pkg->pkgnum ) {
+
+      my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
+      my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
+      my $pkg = $part_pkg->pkg;
+
+      if ( $cust_bill_pkg->setup != 0 ) {
+        push @buf, [ "$pkg Setup",
+                     $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
+        push @buf,
+          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
+      }
+
+      if ( $cust_bill_pkg->recur != 0 ) {
+        push @buf, [
+          "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
+                                time2str("%x", $cust_bill_pkg->edate) . ")",
+          $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
+        ];
+        push @buf,
+          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
+      }
+
+      push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
+
+    } else { #pkgnum tax or one-shot line item
+      my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
+                     ? ( $cust_bill_pkg->itemdesc || 'Tax' )
+                     : 'Tax';
+      if ( $cust_bill_pkg->setup != 0 ) {
+        push @buf, [ $itemdesc,
+                     $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
+      }
+      if ( $cust_bill_pkg->recur != 0 ) {
+        push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
+                                  . time2str("%x", $cust_bill_pkg->edate). ")",
+                     $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
+                   ];
+      }
+    }
+  }
+
+  push @buf,['','-----------'];
+  push @buf,['Total New Charges',
+             $money_char. sprintf("%10.2f",$self->charged) ];
+  push @buf,['',''];
+
+  push @buf,['','-----------'];
+  push @buf,['Total Charges',
+             $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
+  push @buf,['',''];
+
+  #credits
+  foreach ( $self->cust_credited ) {
+
+    #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+
+    my $reason = substr($_->cust_credit->reason,0,32);
+    $reason .= '...' if length($reason) < length($_->cust_credit->reason);
+    $reason = " ($reason) " if $reason;
+    push @buf,[
+      "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
+        $reason,
+      $money_char. sprintf("%10.2f",$_->amount)
+    ];
+  }
+  #foreach ( @cr_cust_credit ) {
+  #  push @buf,[
+  #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
+  #    $money_char. sprintf("%10.2f",$_->credited)
+  #  ];
+  #}
+
+  #get & print payments
+  foreach ( $self->cust_bill_pay ) {
+
+    #something more elaborate if $_->amount ne ->cust_pay->paid ?
+
+    push @buf,[
+      "Payment received ". time2str("%x",$_->cust_pay->_date ),
+      $money_char. sprintf("%10.2f",$_->amount )
+    ];
+  }
+
+  #balance due
+  push @buf,['','-----------'];
+  push @buf,['Balance Due', $money_char. 
+    sprintf("%10.2f", $balance_due ) ];
+
+  #create the template
+  my $templatefile = 'invoice_template';
+  $templatefile .= "_$template" if $template;
+  my @invoice_template = $conf->config($templatefile)
+  or die "cannot load config file $templatefile";
+  $invoice_lines = 0;
+  my $wasfunc = 0;
+  foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
+    /invoice_lines\((\d*)\)/;
+    $invoice_lines += $1 || scalar(@buf);
+    $wasfunc=1;
+  }
+  die "no invoice_lines() functions in template?" unless $wasfunc;
+  my $invoice_template = new Text::Template (
+    TYPE   => 'ARRAY',
+    SOURCE => [ map "$_\n", @invoice_template ],
+  ) or die "can't create new Text::Template object: $Text::Template::ERROR";
+  $invoice_template->compile()
+    or die "can't compile template: $Text::Template::ERROR";
+
+  #setup template variables
+  package FS::cust_bill::_template; #!
+  use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
+
+  $invnum = $self->invnum;
+  $date = $self->_date;
+  $page = 1;
+  $agent = $self->cust_main->agent->agent;
+
+  if ( $FS::cust_bill::invoice_lines ) {
+    $total_pages =
+      int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
+    $total_pages++
+      if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
+  } else {
+    $total_pages = 1;
+  }
+
+  #format address (variable for the template)
+  my $l = 0;
+  @address = ( '', '', '', '', '', '' );
+  package FS::cust_bill; #!
+  $FS::cust_bill::_template::address[$l++] =
+    $cust_main->payname.
+      ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
+        ? " (P.O. #". $cust_main->payinfo. ")"
+        : ''
+      )
+  ;
+  $FS::cust_bill::_template::address[$l++] = $cust_main->company
+    if $cust_main->company;
+  $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
+  $FS::cust_bill::_template::address[$l++] = $cust_main->address2
+    if $cust_main->address2;
+  $FS::cust_bill::_template::address[$l++] =
+    $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
+  $FS::cust_bill::_template::address[$l++] = $cust_main->country
+    unless $cust_main->country eq 'US';
+
+       #  #overdue? (variable for the template)
+       #  $FS::cust_bill::_template::overdue = ( 
+       #    $balance_due > 0
+       #    && $today > $self->_date 
+       ##    && $self->printed > 1
+       #    && $self->printed > 0
+       #  );
+
+  #and subroutine for the template
+  sub FS::cust_bill::_template::invoice_lines {
+    my $lines = shift || scalar(@buf);
+    map { 
+      scalar(@buf) ? shift @buf : [ '', '' ];
+    }
+    ( 1 .. $lines );
+  }
+
+  #and fill it in
+  $FS::cust_bill::_template::page = 1;
+  my $lines;
+  my @collect;
+  while (@buf) {
+    push @collect, split("\n",
+      $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
+    );
+    $FS::cust_bill::_template::page++;
+  }
+
+  map "$_\n", @collect;
+
+}
+
+=back
+
+=head1 BUGS
+
+The delete method.
+
+print_text formatting (and some logic :/) is in source, but needs to be
+slurped in from a file.  Also number of lines ($=).
+
+missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
+or something similar so the look can be completely customized?)
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
+L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_event.pm b/FS/FS/cust_bill_event.pm
new file mode 100644 (file)
index 0000000..c977347
--- /dev/null
@@ -0,0 +1,180 @@
+package FS::cust_bill_event;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_bill;
+use FS::part_bill_event;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::cust_bill_event - Object methods for cust_bill_event records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_event;
+
+  $record = new FS::cust_bill_event \%hash;
+  $record = new FS::cust_bill_event { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_event object represents an complete invoice event.
+FS::cust_bill_event inherits from FS::Record.  The following fields are
+currently supported:
+
+=over 4
+
+=item eventnum - primary key
+
+=item invnum - invoice (see L<FS::cust_bill>)
+
+=item eventpart - event definition (see L<FS::part_bill_event>)
+
+=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=item status - event status: B<done> or B<failed>
+
+=item statustext - additional status detail (i.e. error message)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new completed invoice event.  To add the compelted invoice event to
+the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'cust_bill_event'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid completed invoice event.  If
+there is an error, returns the error, otherwise returns false.  Called by the
+insert and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = $self->ut_numbern('eventnum')
+    || $self->ut_number('invnum')
+    || $self->ut_number('eventpart')
+    || $self->ut_number('_date')
+    || $self->ut_enum('status', [qw( done failed )])
+    || $self->ut_textn('statustext')
+  ;
+
+  return "Unknown invnum ". $self->invnum
+    unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
+
+  return "Unknown eventpart ". $self->eventpart
+    unless qsearchs( 'part_bill_event' ,{ 'eventpart' => $self->eventpart } );
+
+  ''; #no error
+}
+
+=item part_bill_event
+
+Returns the invoice event definition (see L<FS::part_bill_event>) for this
+completed invoice event.
+
+=cut
+
+sub part_bill_event {
+  my $self = shift;
+  qsearchs( 'part_bill_event', { 'eventpart' => $self->eventpart } );
+}
+
+=item cust_bill
+
+Returns the invoice (see L<FS::cust_bill>) for this completed invoice event.
+
+=cut
+
+sub cust_bill {
+  my $self = shift;
+  qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
+}
+
+=item retry
+
+Changes the status of this event from B<done> to B<failed>, allowing it to be
+retried.
+
+=cut
+
+sub retry {
+  my $self = shift;
+  return '' unless $self->status eq 'done';
+  my $old = ref($self)->new( { $self->hash } );
+  $self->status('failed');
+  $self->replace($old);
+}
+
+=back
+
+=head1 BUGS
+
+Far too early in the morning.
+
+=head1 SEE ALSO
+
+L<FS::part_bill_event>, L<FS::cust_bill>, L<FS::Record>, schema.html from the
+base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pay.pm b/FS/FS/cust_bill_pay.pm
new file mode 100644 (file)
index 0000000..913704b
--- /dev/null
@@ -0,0 +1,219 @@
+package FS::cust_bill_pay;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs dbh );
+use FS::cust_bill;
+use FS::cust_pay;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::cust_bill_pay - Object methods for cust_bill_pay records
+
+=head1 SYNOPSIS 
+
+  use FS::cust_bill_pay;
+
+  $record = new FS::cust_bill_pay \%hash;
+  $record = new FS::cust_bill_pay { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pay object represents the application of a payment to a
+specific invoice.  FS::cust_bill_pay inherits from FS::Record.  The following
+fields are currently supported:
+
+=over 4
+
+=item billpaynum - primary key (assigned automatically)
+
+=item invnum - Invoice (see L<FS::cust_bill>)
+
+=item paynum - Payment (see L<FS::cust_pay>)
+
+=item amount - Amount of the payment to apply to the specific invoice.
+
+=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=back
+
+=head1 METHODS
+
+=over 4 
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'cust_bill_pay'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->check;
+  return $error if $error;
+
+  $error = $self->SUPER::insert;
+
+  my $cust_pay = qsearchs('cust_pay', { 'paynum' => $self->paynum } ) or do {
+    $dbh->rollback if $oldAutoCommit;
+    return "unknown cust_pay.paynum: ". $self->paynum;
+  };
+
+  my $pay_total = 0;
+  $pay_total += $_ foreach map { $_->amount }
+    qsearch('cust_bill_pay', { 'paynum' => $self->paynum } );
+
+  if ( sprintf("%.2f", $pay_total) > sprintf("%.2f", $cust_pay->paid) ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "total cust_bill_pay.amount $pay_total for paynum ". $self->paynum.
+           " greater than cust_pay.paid ". $cust_pay->paid;
+  }
+
+  my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } ) or do {
+    $dbh->rollback if $oldAutoCommit;
+    return "unknown cust_bill.invnum: ". $self->invnum;
+  };
+
+  my $bill_total = 0;
+  $bill_total += $_ foreach map { $_->amount }
+    qsearch('cust_bill_pay', { 'invnum' => $self->invnum } );
+  $bill_total += $_ foreach map { $_->amount } 
+    qsearch('cust_credit_bill', { 'invnum' => $self->invnum } );
+  if ( sprintf("%.2f", $bill_total) > sprintf("%.2f", $cust_bill->charged) ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "total cust_bill_pay.amount and cust_credit_bill.amount $bill_total".
+           " for invnum ". $self->invnum.
+           " greater than cust_bill.charged ". $cust_bill->charged;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item delete
+
+Deletes this payment application, unless the closed flag for the parent payment
+(see L<FS::cust_pay>) is set.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  return "Can't delete application for closed payment"
+    if $self->cust_pay->closed =~ /^Y/i;
+  $self->SUPER::delete(@_);
+}
+
+=item replace OLD_RECORD
+
+Currently unimplemented (accounting reasons).
+
+=cut
+
+sub replace {
+   return "Can't (yet?) modify cust_bill_pay records!";
+}
+
+=item check
+
+Checks all fields to make sure this is a valid payment.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert method.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('billpaynum')
+    || $self->ut_number('invnum')
+    || $self->ut_number('paynum')
+    || $self->ut_money('amount')
+    || $self->ut_numbern('_date')
+  ;
+  return $error if $error;
+
+  return "amount must be > 0" if $self->amount <= 0;
+
+  $self->_date(time) unless $self->_date;
+
+  ''; #no error
+}
+
+=item cust_pay 
+
+Returns the payment (see L<FS::cust_pay>)
+
+=cut
+
+sub cust_pay {
+  my $self = shift;
+  qsearchs( 'cust_pay', { 'paynum' => $self->paynum } );
+}
+
+=item cust_bill 
+
+Returns the invoice (see L<FS::cust_bill>)
+
+=cut
+
+sub cust_bill {
+  my $self = shift;
+  qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
+}
+
+=back
+
+=head1 VERSION
+
+$Id: cust_bill_pay.pm,v 1.12 2002-02-07 22:29:34 ivan Exp $
+
+=head1 BUGS
+
+Delete and replace methods.
+
+the checks for over-applied payments could be better done like the ones in
+cust_bill_credit
+
+=head1 SEE ALSO
+
+L<FS::cust_pay>, L<FS::cust_bill>, L<FS::Record>, schema.html from the
+base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm
new file mode 100644 (file)
index 0000000..a6615d0
--- /dev/null
@@ -0,0 +1,215 @@
+package FS::cust_bill_pkg;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs dbdef dbh );
+use FS::cust_pkg;
+use FS::cust_bill;
+use FS::cust_bill_pkg_detail;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::cust_bill_pkg - Object methods for cust_bill_pkg records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_pkg;
+
+  $record = new FS::cust_bill_pkg \%hash;
+  $record = new FS::cust_bill_pkg { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg object represents an invoice line item.
+FS::cust_bill_pkg inherits from FS::Record.  The following fields are currently
+supported:
+
+=over 4
+
+=item invnum - invoice (see L<FS::cust_bill>)
+
+=item pkgnum - package (see L<FS::cust_pkg>) or 0 for the special virtual sales tax package
+
+=item setup - setup fee
+
+=item recur - recurring fee
+
+=item sdate - starting date of recurring fee
+
+=item edate - ending date of recurring fee
+
+=item itemdesc - Line item description (currentlty used only when pkgnum is 0)
+
+=back
+
+sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">.  Also
+see L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new line item.  To add the line item to the database, see
+L<"insert">.  Line items are normally created by calling the bill method of a
+customer object (see L<FS::cust_main>).
+
+=cut
+
+sub table { 'cust_bill_pkg'; }
+
+=item insert
+
+Adds this line item to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  unless ( defined dbdef->table('cust_bill_pkg_detail') && $self->get('details') ) {
+    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    return '';
+  }
+
+  foreach my $detail ( @{$self->get('details')} ) {
+    my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail {
+      'pkgnum' => $self->pkgnum,
+      'invnum' => $self->invnum,
+      'detail' => $detail,
+    };
+    $error = $cust_bill_pkg_detail->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item delete
+
+Currently unimplemented.  I don't remove line items because there would then be
+no record the items ever existed (which is bad, no?)
+
+=cut
+
+sub delete {
+  return "Can't delete cust_bill_pkg records!";
+}
+
+=item replace OLD_RECORD
+
+Currently unimplemented.  This would be even more of an accounting nightmare
+than deleteing the items.  Just don't do it.
+
+=cut
+
+sub replace {
+  return "Can't modify cust_bill_pkg records!";
+}
+
+=item check
+
+Checks all fields to make sure this is a valid line item.  If there is an
+error, returns the error, otherwise returns false.  Called by the insert
+method.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_number('pkgnum')
+      || $self->ut_number('invnum')
+      || $self->ut_money('setup')
+      || $self->ut_money('recur')
+      || $self->ut_numbern('sdate')
+      || $self->ut_numbern('edate')
+      || $self->ut_textn('itemdesc')
+  ;
+  return $error if $error;
+
+  if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
+    return "Unknown pkgnum ". $self->pkgnum
+      unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
+  }
+
+  return "Unknown invnum"
+    unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
+
+  ''; #no error
+}
+
+=item cust_pkg
+
+Returns the package (see L<FS::cust_pkg>) for this invoice line item.
+
+=cut
+
+sub cust_pkg {
+  my $self = shift;
+  qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
+}
+
+=item details
+
+Returns an array of detail information for the invoice line item.
+
+=cut
+
+sub details {
+  my $self = shift;
+  return () unless defined dbdef->table('cust_bill_pkg_detail');
+  map { $_->detail }
+    qsearch ( 'cust_bill_pkg_detail', { 'pkgnum' => $self->pkgnum,
+                                        'invnum' => $self->invnum, } );
+    #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
+from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_detail.pm b/FS/FS/cust_bill_pkg_detail.pm
new file mode 100644 (file)
index 0000000..199de43
--- /dev/null
@@ -0,0 +1,123 @@
+package FS::cust_bill_pkg_detail;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::cust_bill_pkg_detail - Object methods for cust_bill_pkg_detail records
+
+=head1 SYNOPSIS
+
+  use FS::cust_bill_pkg_detail;
+
+  $record = new FS::cust_bill_pkg_detail \%hash;
+  $record = new FS::cust_bill_pkg_detail { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_detail object represents additional detail information for
+an invoice line item (see L<FS::cust_bill_pkg>).  FS::cust_bill_pkg_detail
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item detailnum - primary key
+
+=item pkgnum -
+
+=item invnum -
+
+=item detail - detail description
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new line item detail.  To add the line item detail to the database,
+see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'cust_bill_pkg_detail'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid line item detail.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  $self->ut_numbern('detailnum')
+    || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum')
+    || $self->ut_foreign_key('invnum', 'cust_pkg', 'invnum')
+    || $self->ut_text('detail')
+  ;
+
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::cust_bill_pkg>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm
new file mode 100644 (file)
index 0000000..284d59d
--- /dev/null
@@ -0,0 +1,260 @@
+package FS::cust_credit;
+
+use strict;
+use vars qw( @ISA $conf $unsuspendauto );
+use FS::UID qw( dbh getotaker );
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_main;
+use FS::cust_refund;
+use FS::cust_credit_bill;
+
+@ISA = qw( FS::Record );
+
+#ask FS::UID to run this stuff for us later
+$FS::UID::callback{'FS::cust_credit'} = sub { 
+
+  $conf = new FS::Conf;
+  $unsuspendauto = $conf->exists('unsuspendauto');
+
+};
+
+=head1 NAME
+
+FS::cust_credit - Object methods for cust_credit records
+
+=head1 SYNOPSIS
+
+  use FS::cust_credit;
+
+  $record = new FS::cust_credit \%hash;
+  $record = new FS::cust_credit { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_credit object represents a credit; the equivalent of a negative
+B<cust_bill> record (see L<FS::cust_bill>).  FS::cust_credit inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item crednum - primary key (assigned automatically for new credits)
+
+=item custnum - customer (see L<FS::cust_main>)
+
+=item amount - amount of the credit
+
+=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=item otaker - order taker (assigned automatically, see L<FS::UID>)
+
+=item reason - text
+
+=item closed - books closed flag, empty or `Y'
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new credit.  To add the credit to the database, see L<"insert">.
+
+=cut
+
+sub table { 'cust_credit'; }
+
+=item insert
+
+Adds this credit to the database ("Posts" the credit).  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $cust_main = qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+  my $old_balance = $cust_main->balance;
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "error inserting $self: $error";
+  }
+
+  #false laziness w/ cust_credit::insert
+  if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
+    my @errors = $cust_main->unsuspend;
+    #return 
+    # side-fx with nested transactions?  upstack rolls back?
+    warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
+         join(' / ', @errors)
+      if @errors;
+  }
+  #eslaf
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+=item delete
+
+Currently unimplemented.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  return "Can't delete closed credit" if $self->closed =~ /^Y/i;
+  $self->SUPER::delete(@_);
+}
+
+=item replace OLD_RECORD
+
+Credits may not be modified; there would then be no record the credit was ever
+posted.
+
+=cut
+
+sub replace {
+  return "Can't modify credit!"
+}
+
+=item check
+
+Checks all fields to make sure this is a valid credit.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('crednum')
+    || $self->ut_number('custnum')
+    || $self->ut_numbern('_date')
+    || $self->ut_money('amount')
+    || $self->ut_textn('reason')
+    || $self->ut_enum('closed', [ '', 'Y' ])
+  ;
+  return $error if $error;
+
+  return "amount must be > 0 " if $self->amount <= 0;
+
+  return "Unknown customer"
+    unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+
+  $self->_date(time) unless $self->_date;
+
+  $self->otaker(getotaker);
+
+  ''; #no error
+}
+
+=item cust_refund
+
+Depreciated.  See the cust_credit_refund method.
+
+#Returns all refunds (see L<FS::cust_refund>) for this credit.
+
+=cut
+
+sub cust_refund {
+  use Carp;
+  croak "FS::cust_credit->cust_pay depreciated; see ".
+        "FS::cust_credit->cust_credit_refund";
+  #my $self = shift;
+  #sort { $a->_date <=> $b->_date }
+  #  qsearch( 'cust_refund', { 'crednum' => $self->crednum } )
+  #;
+}
+
+=item cust_credit_refund
+
+Returns all refund applications (see L<FS::cust_credit_refund>) for this credit.
+
+=cut
+
+sub cust_credit_refund {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_credit_refund', { 'crednum' => $self->crednum } )
+  ;
+}
+
+=item cust_credit_bill
+
+Returns all application to invoices (see L<FS::cust_credit_bill>) for this
+credit.
+
+=cut
+
+sub cust_credit_bill {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_credit_bill', { 'crednum' => $self->crednum } )
+  ;
+}
+
+=item credited
+
+Returns the amount of this credit that is still outstanding; which is
+amount minus all refund applications (see L<FS::cust_credit_refund>) and
+applications to invoices (see L<FS::cust_credit_bill>).
+
+=cut
+
+sub credited {
+  my $self = shift;
+  my $amount = $self->amount;
+  $amount -= $_->amount foreach ( $self->cust_credit_refund );
+  $amount -= $_->amount foreach ( $self->cust_credit_bill );
+  sprintf( "%.2f", $amount );
+}
+
+=back
+
+=head1 VERSION
+
+$Id: cust_credit.pm,v 1.16 2002-06-04 14:35:52 ivan Exp $
+
+=head1 BUGS
+
+The delete method.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_credit_refund>, L<FS::cust_refund>,
+L<FS::cust_credit_bill> L<FS::cust_bill>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_credit_bill.pm b/FS/FS/cust_credit_bill.pm
new file mode 100644 (file)
index 0000000..6221541
--- /dev/null
@@ -0,0 +1,162 @@
+package FS::cust_credit_bill;
+
+use strict;
+use vars qw( @ISA );
+use FS::UID qw( getotaker );
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_main;
+#use FS::cust_refund;
+use FS::cust_credit;
+use FS::cust_bill;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::cust_credit_bill - Object methods for cust_credit_bill records
+
+=head1 SYNOPSIS
+
+  use FS::cust_credit_bill;
+
+  $record = new FS::cust_credit_bill \%hash;
+  $record = new FS::cust_credit_bill { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_credit_bill object represents application of a credit (see
+L<FS::cust_credit>) to an invoice (see L<FS::cust_bill>).  FS::cust_credit
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item creditbillnum - primary key
+
+=item crednum - credit being applied 
+
+=item invnum - invoice to which credit is applied (see L<FS::cust_bill>)
+
+=item amount - amount of the credit applied
+
+=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new cust_credit_bill.  To add the cust_credit_bill to the database,
+see L<"insert">.
+
+=cut
+
+sub table { 'cust_credit_bill'; }
+
+=item insert
+
+Adds this cust_credit_bill to the database ("Posts" all or part of a credit).
+If there is an error, returns the error, otherwise returns false.
+
+=item delete
+
+Currently unimplemented.
+
+=cut
+
+sub delete {
+  return "Can't unapply credit!"
+}
+
+=item replace OLD_RECORD
+
+Application of credits may not be modified.
+
+=cut
+
+sub replace {
+  return "Can't modify application of credit!"
+}
+
+=item check
+
+Checks all fields to make sure this is a valid credit application.  If there
+is an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('creditbillnum')
+    || $self->ut_number('crednum')
+    || $self->ut_number('invnum')
+    || $self->ut_numbern('_date')
+    || $self->ut_money('amount')
+  ;
+  return $error if $error;
+
+  return "amount must be > 0" if $self->amount <= 0;
+
+  return "Unknown credit"
+    unless my $cust_credit = 
+      qsearchs( 'cust_credit', { 'crednum' => $self->crednum } );
+
+  return "Unknown invoice"
+    unless my $cust_bill =
+      qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
+
+  $self->_date(time) unless $self->_date;
+
+  return "Cannot apply more than remaining value of credit"
+    unless $self->amount <= $cust_credit->credited;
+
+  return "Cannot apply more than remaining value of invoice"
+    unless $self->amount <= $cust_bill->owed;
+
+  ''; #no error
+}
+
+=item sub cust_credit
+
+Returns the credit (see L<FS::cust_credit>)
+
+=cut
+
+sub cust_credit {
+  my $self = shift;
+  qsearchs( 'cust_credit', { 'crednum' => $self->crednum } );
+}
+
+=back
+
+=head1 VERSION
+
+$Id: cust_credit_bill.pm,v 1.7 2002-01-24 16:58:47 ivan Exp $
+
+=head1 BUGS
+
+The delete method.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_refund>, L<FS::cust_bill>, L<FS::cust_credit>,
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_credit_refund.pm b/FS/FS/cust_credit_refund.pm
new file mode 100644 (file)
index 0000000..cc3b32c
--- /dev/null
@@ -0,0 +1,205 @@
+package FS::cust_credit_refund;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs dbh );
+#use FS::UID qw(getotaker);
+use FS::cust_credit;
+use FS::cust_refund;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::cust_credit_refund - Object methods for cust_bill_pay records
+
+=head1 SYNOPSIS 
+
+  use FS::cust_credit_refund;
+
+  $record = new FS::cust_credit_refund \%hash;
+  $record = new FS::cust_credit_refund { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_credit_refund represents the application of a refund to a specific
+credit.  FS::cust_credit_refund inherits from FS::Record.  The following fields
+are currently supported:
+
+=over 4
+
+=item creditrefundnum - primary key (assigned automatically)
+
+=item crednum - Credit (see L<FS::cust_credit>)
+
+=item refundnum - Refund (see L<FS::cust_refund>)
+
+=item amount - Amount of the refund to apply to the specific credit.
+
+=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=back
+
+=head1 METHODS
+
+=over 4 
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'cust_credit_refund'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->check;
+  return $error if $error;
+
+  $error = $self->SUPER::insert;
+
+  my $cust_refund =
+    qsearchs('cust_refund', { 'refundnum' => $self->refundnum } )
+  or do {
+    $dbh->rollback if $oldAutoCommit;
+    return "unknown cust_refund.refundnum: ". $self->refundnum
+  };
+
+  my $refund_total = 0;
+  $refund_total += $_ foreach map { $_->amount }
+    qsearch('cust_credit_refund', { 'refundnum' => $self->refundnum } );
+
+  if ( $refund_total > $cust_refund->refund ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "total cust_credit_refund.amount $refund_total for refundnum ".
+           $self->refundnum.
+           " greater than cust_refund.refund ". $cust_refund->refund;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item delete
+
+Currently unimplemented (accounting reasons).
+
+=cut
+
+sub delete {
+  return "Can't (yet?) delete cust_credit_refund records!";
+}
+
+=item replace OLD_RECORD
+
+Currently unimplemented (accounting reasons).
+
+=cut
+
+sub replace {
+   return "Can't (yet?) modify cust_credit_refund records!";
+}
+
+=item check
+
+Checks all fields to make sure this is a valid payment.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert method.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('creditrefundnum')
+    || $self->ut_number('crednum')
+    || $self->ut_number('refundnum')
+    || $self->ut_money('amount')
+    || $self->ut_numbern('_date')
+  ;
+  return $error if $error;
+
+  return "amount must be > 0" if $self->amount <= 0;
+
+  $self->_date(time) unless $self->_date;
+
+  return "unknown cust_credit.crednum: ". $self->crednum
+    unless qsearchs( 'cust_credit', { 'crednum' => $self->crednum } );
+
+  ''; #no error
+}
+
+=item cust_refund
+
+Returns the refund (see L<FS::cust_refund>)
+
+=cut
+
+sub cust_refund {
+  my $self = shift;
+  qsearchs( 'cust_refund', { 'refundnum' => $self->refundnum } );
+}
+
+=item cust_credit
+
+Returns the credit (see L<FS::cust_credit>)
+
+=cut
+
+sub cust_credit {
+  my $self = shift;
+  qsearchs( 'cust_credit', { 'crednum' => $self->crednum } );
+}
+
+=back
+
+=head1 VERSION
+
+$Id: cust_credit_refund.pm,v 1.9 2002-01-26 01:52:31 ivan Exp $
+
+=head1 BUGS
+
+Delete and replace methods.
+
+the checks for over-applied refunds could be better done like the ones in
+cust_bill_credit
+
+=head1 SEE ALSO
+
+L<FS::cust_credit>, L<FS::cust_refund>, L<FS::Record>, schema.html from the
+base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
new file mode 100644 (file)
index 0000000..4302c50
--- /dev/null
@@ -0,0 +1,2559 @@
+package FS::cust_main;
+
+use strict;
+use vars qw( @ISA $conf $Debug $import );
+use Safe;
+use Carp;
+BEGIN {
+  eval "use Time::Local;";
+  die "Time::Local version 1.05 required with Perl versions before 5.6"
+    if $] < 5.006 && !defined($Time::Local::VERSION);
+  eval "use Time::Local qw(timelocal timelocal_nocheck);";
+}
+use Date::Format;
+#use Date::Manip;
+use Business::CreditCard;
+use FS::UID qw( getotaker dbh );
+use FS::Record qw( qsearchs qsearch dbdef );
+use FS::Misc qw( send_email );
+use FS::cust_pkg;
+use FS::cust_bill;
+use FS::cust_bill_pkg;
+use FS::cust_pay;
+use FS::cust_credit;
+use FS::part_referral;
+use FS::cust_main_county;
+use FS::agent;
+use FS::cust_main_invoice;
+use FS::cust_credit_bill;
+use FS::cust_bill_pay;
+use FS::prepay_credit;
+use FS::queue;
+use FS::part_pkg;
+use FS::part_bill_event;
+use FS::cust_bill_event;
+use FS::cust_tax_exempt;
+use FS::type_pkgs;
+use FS::Msgcat qw(gettext);
+
+@ISA = qw( FS::Record );
+
+$Debug = 1;
+#$Debug = 1;
+
+$import = 0;
+
+#ask FS::UID to run this stuff for us later
+#$FS::UID::callback{'FS::cust_main'} = sub { 
+install_callback FS::UID sub { 
+  $conf = new FS::Conf;
+  #yes, need it for stuff below (prolly should be cached)
+};
+
+sub _cache {
+  my $self = shift;
+  my ( $hashref, $cache ) = @_;
+  if ( exists $hashref->{'pkgnum'} ) {
+#    #@{ $self->{'_pkgnum'} } = ();
+    my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
+    $self->{'_pkgnum'} = $subcache;
+    #push @{ $self->{'_pkgnum'} },
+    FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
+  }
+}
+
+=head1 NAME
+
+FS::cust_main - Object methods for cust_main records
+
+=head1 SYNOPSIS
+
+  use FS::cust_main;
+
+  $record = new FS::cust_main \%hash;
+  $record = new FS::cust_main { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  @cust_pkg = $record->all_pkgs;
+
+  @cust_pkg = $record->ncancelled_pkgs;
+
+  @cust_pkg = $record->suspended_pkgs;
+
+  $error = $record->bill;
+  $error = $record->bill %options;
+  $error = $record->bill 'time' => $time;
+
+  $error = $record->collect;
+  $error = $record->collect %options;
+  $error = $record->collect 'invoice_time'   => $time,
+                            'batch_card'     => 'yes',
+                            'report_badcard' => 'yes',
+                          ;
+
+=head1 DESCRIPTION
+
+An FS::cust_main object represents a customer.  FS::cust_main inherits from 
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item custnum - primary key (assigned automatically for new customers)
+
+=item agentnum - agent (see L<FS::agent>)
+
+=item refnum - Advertising source (see L<FS::part_referral>)
+
+=item first - name
+
+=item last - name
+
+=item ss - social security number (optional)
+
+=item company - (optional)
+
+=item address1
+
+=item address2 - (optional)
+
+=item city
+
+=item county - (optional, see L<FS::cust_main_county>)
+
+=item state - (see L<FS::cust_main_county>)
+
+=item zip
+
+=item country - (see L<FS::cust_main_county>)
+
+=item daytime - phone (optional)
+
+=item night - phone (optional)
+
+=item fax - phone (optional)
+
+=item ship_first - name
+
+=item ship_last - name
+
+=item ship_company - (optional)
+
+=item ship_address1
+
+=item ship_address2 - (optional)
+
+=item ship_city
+
+=item ship_county - (optional, see L<FS::cust_main_county>)
+
+=item ship_state - (see L<FS::cust_main_county>)
+
+=item ship_zip
+
+=item ship_country - (see L<FS::cust_main_county>)
+
+=item ship_daytime - phone (optional)
+
+=item ship_night - phone (optional)
+
+=item ship_fax - phone (optional)
+
+=item payby - I<CARD> (credit card - automatic), I<DCRD> (credit card - on-demand), I<CHEK> (electronic check - automatic), I<DCHK> (electronic check - on-demand), I<LECB> (Phone bill billing), I<BILL> (billing), I<COMP> (free), or I<PREPAY> (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to I<BILL>)
+
+=item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
+
+=item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
+
+=item payname - name on card or billing name
+
+=item tax - tax exempt, empty or `Y'
+
+=item otaker - order taker (assigned automatically, see L<FS::UID>)
+
+=item comments - comments (optional)
+
+=item referral_custnum - referring customer number
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new customer.  To add the customer to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_main'; }
+
+=item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
+
+Adds this customer to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
+method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
+are inserted atomicly, or the transaction is rolled back.  Passing an empty
+hash reference is equivalent to not supplying this parameter.  There should be
+a better explanation of this, but until then, here's an example:
+
+  use Tie::RefHash;
+  tie %hash, 'Tie::RefHash'; #this part is important
+  %hash = (
+    $cust_pkg => [ $svc_acct ],
+    ...
+  );
+  $cust_main->insert( \%hash );
+
+INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
+be set as the invoicing list (see L<"invoicing_list">).  Errors return as
+expected and rollback the entire transaction; it is not necessary to call 
+check_invoicing_list first.  The invoicing_list is set after the records in the
+CUST_PKG_HASHREF above are inserted, so it is now possible to set an
+invoicing_list destination to the newly-created svc_acct.  Here's an example:
+
+  $cust_main->insert( {}, [ $email, 'POST' ] );
+
+=cut
+
+sub insert {
+  my $self = shift;
+  my $cust_pkgs = @_ ? shift : {};
+  my $invoicing_list = @_ ? shift : '';
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $amount = 0;
+  my $seconds = 0;
+  if ( $self->payby eq 'PREPAY' ) {
+    $self->payby('BILL');
+    my $prepay_credit = qsearchs(
+      'prepay_credit',
+      { 'identifier' => $self->payinfo },
+      '',
+      'FOR UPDATE'
+    );
+    warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
+      unless $prepay_credit;
+    $amount = $prepay_credit->amount;
+    $seconds = $prepay_credit->seconds;
+    my $error = $prepay_credit->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "removing prepay_credit (transaction rolled back): $error";
+    }
+  }
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    #return "inserting cust_main record (transaction rolled back): $error";
+    return $error;
+  }
+
+  # invoicing list
+  if ( $invoicing_list ) {
+    $error = $self->check_invoicing_list( $invoicing_list );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "checking invoicing_list (transaction rolled back): $error";
+    }
+    $self->invoicing_list( $invoicing_list );
+  }
+
+  # packages
+  $error = $self->order_pkgs($cust_pkgs, \$seconds);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $seconds ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "No svc_acct record to apply pre-paid time";
+  }
+
+  if ( $amount ) {
+    my $cust_credit = new FS::cust_credit {
+      'custnum' => $self->custnum,
+      'amount'  => $amount,
+    };
+    $error = $cust_credit->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "inserting credit (transaction rolled back): $error";
+    }
+  }
+
+  $error = $self->queue_fuzzyfiles_update;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "updating fuzzy search cache: $error";
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item order_pkgs
+
+document me.  like ->insert(%cust_pkg) on an existing record
+
+=cut
+
+sub order_pkgs {
+  my $self = shift;
+  my $cust_pkgs = shift;
+  my $seconds = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_pkg ( keys %$cust_pkgs ) {
+    $cust_pkg->custnum( $self->custnum );
+    my $error = $cust_pkg->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "inserting cust_pkg (transaction rolled back): $error";
+    }
+    foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
+      $svc_something->pkgnum( $cust_pkg->pkgnum );
+      if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
+        $svc_something->seconds( $svc_something->seconds + $$seconds );
+        $$seconds = 0;
+      }
+      $error = $svc_something->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        #return "inserting svc_ (transaction rolled back): $error";
+        return $error;
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+}
+
+=item delete NEW_CUSTNUM
+
+This deletes the customer.  If there is an error, returns the error, otherwise
+returns false.
+
+This will completely remove all traces of the customer record.  This is not
+what you want when a customer cancels service; for that, cancel all of the
+customer's packages (see L<FS::cust_pkg/cancel>).
+
+If the customer has any uncancelled packages, you need to pass a new (valid)
+customer number for those packages to be transferred to.  Cancelled packages
+will be deleted.  Did I mention that this is NOT what you want when a customer
+cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
+
+You can't delete a customer with invoices (see L<FS::cust_bill>),
+or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
+refunds (see L<FS::cust_refund>).
+
+=cut
+
+sub delete {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Can't delete a customer with invoices";
+  }
+  if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Can't delete a customer with credits";
+  }
+  if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Can't delete a customer with payments";
+  }
+  if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Can't delete a customer with refunds";
+  }
+
+  my @cust_pkg = $self->ncancelled_pkgs;
+  if ( @cust_pkg ) {
+    my $new_custnum = shift;
+    unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Invalid new customer number: $new_custnum";
+    }
+    foreach my $cust_pkg ( @cust_pkg ) {
+      my %hash = $cust_pkg->hash;
+      $hash{'custnum'} = $new_custnum;
+      my $new_cust_pkg = new FS::cust_pkg ( \%hash );
+      my $error = $new_cust_pkg->replace($cust_pkg);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+  my @cancelled_cust_pkg = $self->all_pkgs;
+  foreach my $cust_pkg ( @cancelled_cust_pkg ) {
+    my $error = $cust_pkg->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
+    qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
+  ) {
+    my $error = $cust_main_invoice->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
+be set as the invoicing list (see L<"invoicing_list">).  Errors return as
+expected and rollback the entire transaction; it is not necessary to call 
+check_invoicing_list first.  Here's an example:
+
+  $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
+
+=cut
+
+sub replace {
+  my $self = shift;
+  my $old = shift;
+  my @param = @_;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::replace($old);
+
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( @param ) { # INVOICING_LIST_ARYREF
+    my $invoicing_list = shift @param;
+    $error = $self->check_invoicing_list( $invoicing_list );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+    $self->invoicing_list( $invoicing_list );
+  }
+
+  if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
+       grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
+    # card/check/lec info has changed, want to retry realtime_ invoice events
+    my $error = $self->retry_realtime;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $error = $self->queue_fuzzyfiles_update;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "updating fuzzy search cache: $error";
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item queue_fuzzyfiles_update
+
+Used by insert & replace to update the fuzzy search cache
+
+=cut
+
+sub queue_fuzzyfiles_update {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
+  my $error = $queue->insert($self->getfield('last'), $self->company);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "queueing job (transaction rolled back): $error";
+  }
+
+  if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
+    $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
+    $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "queueing job (transaction rolled back): $error";
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item check
+
+Checks all fields to make sure this is a valid customer record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and repalce methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  #warn "BEFORE: \n". $self->_dump;
+
+  my $error =
+    $self->ut_numbern('custnum')
+    || $self->ut_number('agentnum')
+    || $self->ut_number('refnum')
+    || $self->ut_name('last')
+    || $self->ut_name('first')
+    || $self->ut_textn('company')
+    || $self->ut_text('address1')
+    || $self->ut_textn('address2')
+    || $self->ut_text('city')
+    || $self->ut_textn('county')
+    || $self->ut_textn('state')
+    || $self->ut_country('country')
+    || $self->ut_anything('comments')
+    || $self->ut_numbern('referral_custnum')
+  ;
+  #barf.  need message catalogs.  i18n.  etc.
+  $error .= "Please select a advertising source."
+    if $error =~ /^Illegal or empty \(numeric\) refnum: /;
+  return $error if $error;
+
+  return "Unknown agent"
+    unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
+
+  return "Unknown refnum"
+    unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
+
+  return "Unknown referring custnum ". $self->referral_custnum
+    unless ! $self->referral_custnum 
+           || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
+
+  if ( $self->ss eq '' ) {
+    $self->ss('');
+  } else {
+    my $ss = $self->ss;
+    $ss =~ s/\D//g;
+    $ss =~ /^(\d{3})(\d{2})(\d{4})$/
+      or return "Illegal social security number: ". $self->ss;
+    $self->ss("$1-$2-$3");
+  }
+
+
+# bad idea to disable, causes billing to fail because of no tax rates later
+#  unless ( $import ) {
+    unless ( qsearch('cust_main_county', {
+      'country' => $self->country,
+      'state'   => '',
+     } ) ) {
+      return "Unknown state/county/country: ".
+        $self->state. "/". $self->county. "/". $self->country
+        unless qsearch('cust_main_county',{
+          'state'   => $self->state,
+          'county'  => $self->county,
+          'country' => $self->country,
+        } );
+    }
+#  }
+
+  $error =
+    $self->ut_phonen('daytime', $self->country)
+    || $self->ut_phonen('night', $self->country)
+    || $self->ut_phonen('fax', $self->country)
+    || $self->ut_zip('zip', $self->country)
+  ;
+  return $error if $error;
+
+  my @addfields = qw(
+    last first company address1 address2 city county state zip
+    country daytime night fax
+  );
+
+  if ( defined $self->dbdef_table->column('ship_last') ) {
+    if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
+                       @addfields )
+         && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
+       )
+    {
+      my $error =
+        $self->ut_name('ship_last')
+        || $self->ut_name('ship_first')
+        || $self->ut_textn('ship_company')
+        || $self->ut_text('ship_address1')
+        || $self->ut_textn('ship_address2')
+        || $self->ut_text('ship_city')
+        || $self->ut_textn('ship_county')
+        || $self->ut_textn('ship_state')
+        || $self->ut_country('ship_country')
+      ;
+      return $error if $error;
+
+      #false laziness with above
+      unless ( qsearchs('cust_main_county', {
+        'country' => $self->ship_country,
+        'state'   => '',
+       } ) ) {
+        return "Unknown ship_state/ship_county/ship_country: ".
+          $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
+          unless qsearchs('cust_main_county',{
+            'state'   => $self->ship_state,
+            'county'  => $self->ship_county,
+            'country' => $self->ship_country,
+          } );
+      }
+      #eofalse
+
+      $error =
+        $self->ut_phonen('ship_daytime', $self->ship_country)
+        || $self->ut_phonen('ship_night', $self->ship_country)
+        || $self->ut_phonen('ship_fax', $self->ship_country)
+        || $self->ut_zip('ship_zip', $self->ship_country)
+      ;
+      return $error if $error;
+
+    } else { # ship_ info eq billing info, so don't store dup info in database
+      $self->setfield("ship_$_", '')
+        foreach qw( last first company address1 address2 city county state zip
+                    country daytime night fax );
+    }
+  }
+
+  $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
+    or return "Illegal payby: ". $self->payby;
+  $self->payby($1);
+
+  if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
+
+    my $payinfo = $self->payinfo;
+    $payinfo =~ s/\D//g;
+    $payinfo =~ /^(\d{13,16})$/
+      or return gettext('invalid_card'); # . ": ". $self->payinfo;
+    $payinfo = $1;
+    $self->payinfo($payinfo);
+    validate($payinfo)
+      or return gettext('invalid_card'); # . ": ". $self->payinfo;
+    return gettext('unknown_card_type')
+      if cardtype($self->payinfo) eq "Unknown";
+
+  } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
+
+    my $payinfo = $self->payinfo;
+    $payinfo =~ s/[^\d\@]//g;
+    $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
+    $payinfo = "$1\@$2";
+    $self->payinfo($payinfo);
+
+  } elsif ( $self->payby eq 'LECB' ) {
+
+    my $payinfo = $self->payinfo;
+    $payinfo =~ s/\D//g;
+    $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
+    $payinfo = $1;
+    $self->payinfo($payinfo);
+
+  } elsif ( $self->payby eq 'BILL' ) {
+
+    $error = $self->ut_textn('payinfo');
+    return "Illegal P.O. number: ". $self->payinfo if $error;
+
+  } elsif ( $self->payby eq 'COMP' ) {
+
+    $error = $self->ut_textn('payinfo');
+    return "Illegal comp account issuer: ". $self->payinfo if $error;
+
+  } elsif ( $self->payby eq 'PREPAY' ) {
+
+    my $payinfo = $self->payinfo;
+    $payinfo =~ s/\W//g; #anything else would just confuse things
+    $self->payinfo($payinfo);
+    $error = $self->ut_alpha('payinfo');
+    return "Illegal prepayment identifier: ". $self->payinfo if $error;
+    return "Unknown prepayment identifier"
+      unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
+
+  }
+
+  if ( $self->paydate eq '' || $self->paydate eq '-' ) {
+    return "Expriation date required"
+      unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
+    $self->paydate('');
+  } else {
+    my( $m, $y );
+    if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
+      ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
+    } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{2})[\/\-]\d+$/ ) {
+      ( $m, $y ) = ( $3, "20$2" );
+    } else {
+      return "Illegal expiration date: ". $self->paydate;
+    }
+    $self->paydate("$y-$m-01");
+    my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
+    return gettext('expired_card')
+      if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
+  }
+
+  if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
+       ( ! $conf->exists('require_cardname')
+         || $self->payby !~ /^(CARD|DCRD)$/  ) 
+  ) {
+    $self->payname( $self->first. " ". $self->getfield('last') );
+  } else {
+    $self->payname =~ /^([\w \,\.\-\']+)$/
+      or return gettext('illegal_name'). " payname: ". $self->payname;
+    $self->payname($1);
+  }
+
+  $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
+  $self->tax($1);
+
+  $self->otaker(getotaker);
+
+  #warn "AFTER: \n". $self->_dump;
+
+  ''; #no error
+}
+
+=item all_pkgs
+
+Returns all packages (see L<FS::cust_pkg>) for this customer.
+
+=cut
+
+sub all_pkgs {
+  my $self = shift;
+  if ( $self->{'_pkgnum'} ) {
+    values %{ $self->{'_pkgnum'}->cache };
+  } else {
+    qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
+  }
+}
+
+=item ncancelled_pkgs
+
+Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
+
+=cut
+
+sub ncancelled_pkgs {
+  my $self = shift;
+  if ( $self->{'_pkgnum'} ) {
+    grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
+  } else {
+    @{ [ # force list context
+      qsearch( 'cust_pkg', {
+        'custnum' => $self->custnum,
+        'cancel'  => '',
+      }),
+      qsearch( 'cust_pkg', {
+        'custnum' => $self->custnum,
+        'cancel'  => 0,
+      }),
+    ] };
+  }
+}
+
+=item suspended_pkgs
+
+Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
+
+=cut
+
+sub suspended_pkgs {
+  my $self = shift;
+  grep { $_->susp } $self->ncancelled_pkgs;
+}
+
+=item unflagged_suspended_pkgs
+
+Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
+customer (thouse packages without the `manual_flag' set).
+
+=cut
+
+sub unflagged_suspended_pkgs {
+  my $self = shift;
+  return $self->suspended_pkgs
+    unless dbdef->table('cust_pkg')->column('manual_flag');
+  grep { ! $_->manual_flag } $self->suspended_pkgs;
+}
+
+=item unsuspended_pkgs
+
+Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
+this customer.
+
+=cut
+
+sub unsuspended_pkgs {
+  my $self = shift;
+  grep { ! $_->susp } $self->ncancelled_pkgs;
+}
+
+=item unsuspend
+
+Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
+and L<FS::cust_pkg>) for this customer.  Always returns a list: an empty list
+on success or a list of errors.
+
+=cut
+
+sub unsuspend {
+  my $self = shift;
+  grep { $_->unsuspend } $self->suspended_pkgs;
+}
+
+=item suspend
+
+Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
+Always returns a list: an empty list on success or a list of errors.
+
+=cut
+
+sub suspend {
+  my $self = shift;
+  grep { $_->suspend } $self->unsuspended_pkgs;
+}
+
+=item cancel
+
+Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
+Always returns a list: an empty list on success or a list of errors.
+
+=cut
+
+sub cancel {
+  my $self = shift;
+  grep { $_->cancel } $self->ncancelled_pkgs;
+}
+
+=item agent
+
+Returns the agent (see L<FS::agent>) for this customer.
+
+=cut
+
+sub agent {
+  my $self = shift;
+  qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
+}
+
+=item bill OPTIONS
+
+Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
+conjunction with the collect method.
+
+Options are passed as name-value pairs.
+
+The only currently available option is `time', which bills the customer as if
+it were that time.  It is specified as a UNIX timestamp; see
+L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
+functions.  For example:
+
+ use Date::Parse;
+ ...
+ $cust_main->bill( 'time' => str2time('April 20th, 2001') );
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub bill {
+  my( $self, %options ) = @_;
+  my $time = $options{'time'} || time;
+
+  my $error;
+
+  #put below somehow?
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  # find the packages which are due for billing, find out how much they are
+  # & generate invoice database.
+  my( $total_setup, $total_recur ) = ( 0, 0 );
+  #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
+  my @cust_bill_pkg = ();
+  #my $tax = 0;##
+  #my $taxable_charged = 0;##
+  #my $charged = 0;##
+
+  my %tax;
+
+  foreach my $cust_pkg (
+    qsearch('cust_pkg', { 'custnum' => $self->custnum } )
+  ) {
+
+    #NO!! next if $cust_pkg->cancel;  
+    next if $cust_pkg->getfield('cancel');  
+
+    #? to avoid use of uninitialized value errors... ?
+    $cust_pkg->setfield('bill', '')
+      unless defined($cust_pkg->bill);
+    my $part_pkg = $cust_pkg->part_pkg;
+
+    #so we don't modify cust_pkg record unnecessarily
+    my $cust_pkg_mod_flag = 0;
+    my %hash = $cust_pkg->hash;
+    my $old_cust_pkg = new FS::cust_pkg \%hash;
+
+    my @details = ();
+
+    # bill setup
+    my $setup = 0;
+    unless ( $cust_pkg->setup ) {
+      my $setup_prog = $part_pkg->getfield('setup');
+      $setup_prog =~ /^(.*)$/ or do {
+        $dbh->rollback if $oldAutoCommit;
+        return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
+               ": $setup_prog";
+      };
+      $setup_prog = $1;
+      $setup_prog = '0' if $setup_prog =~ /^\s*$/;
+
+        #my $cpt = new Safe;
+        ##$cpt->permit(); #what is necessary?
+        #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
+        #$setup = $cpt->reval($setup_prog);
+      $setup = eval $setup_prog;
+      unless ( defined($setup) ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
+               "(expression $setup_prog): $@";
+      }
+      $cust_pkg->setfield('setup',$time);
+      $cust_pkg_mod_flag=1; 
+    }
+
+    #bill recurring fee
+    my $recur = 0;
+    my $sdate;
+    if ( $part_pkg->getfield('freq') > 0 &&
+         ! $cust_pkg->getfield('susp') &&
+         ( $cust_pkg->getfield('bill') || 0 ) <= $time
+    ) {
+      my $recur_prog = $part_pkg->getfield('recur');
+      $recur_prog =~ /^(.*)$/ or do {
+        $dbh->rollback if $oldAutoCommit;
+        return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
+               ": $recur_prog";
+      };
+      $recur_prog = $1;
+      $recur_prog = '0' if $recur_prog =~ /^\s*$/;
+
+      # shared with $recur_prog
+      $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
+
+        #my $cpt = new Safe;
+        ##$cpt->permit(); #what is necessary?
+        #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
+        #$recur = $cpt->reval($recur_prog);
+      $recur = eval $recur_prog;
+      unless ( defined($recur) ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "Error eval-ing part_pkg->recur pkgpart ".  $part_pkg->pkgpart.
+               "(expression $recur_prog): $@";
+      }
+      #change this bit to use Date::Manip? CAREFUL with timezones (see
+      # mailing list archive)
+      my ($sec,$min,$hour,$mday,$mon,$year) =
+        (localtime($sdate) )[0,1,2,3,4,5];
+
+      #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
+      # only for figuring next bill date, nothing else, so, reset $sdate again
+      # here
+      $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
+      $cust_pkg->last_bill($sdate)
+        if $cust_pkg->dbdef_table->column('last_bill');
+
+      $mon += $part_pkg->freq;
+      until ( $mon < 12 ) { $mon -= 12; $year++; }
+      $cust_pkg->setfield('bill',
+        timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
+      $cust_pkg_mod_flag = 1; 
+    }
+
+    warn "\$setup is undefined" unless defined($setup);
+    warn "\$recur is undefined" unless defined($recur);
+    warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
+
+    my $taxable_charged = 0;
+    if ( $cust_pkg_mod_flag ) {
+      $error=$cust_pkg->replace($old_cust_pkg);
+      if ( $error ) { #just in case
+        $dbh->rollback if $oldAutoCommit;
+        return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
+      }
+      $setup = sprintf( "%.2f", $setup );
+      $recur = sprintf( "%.2f", $recur );
+      if ( $setup < 0 ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
+      }
+      if ( $recur < 0 ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
+      }
+      if ( $setup > 0 || $recur > 0 ) {
+        my $cust_bill_pkg = new FS::cust_bill_pkg ({
+          'pkgnum'  => $cust_pkg->pkgnum,
+          'setup'   => $setup,
+          'recur'   => $recur,
+          'sdate'   => $sdate,
+          'edate'   => $cust_pkg->bill,
+          'details' => \@details,
+        });
+        push @cust_bill_pkg, $cust_bill_pkg;
+        $total_setup += $setup;
+        $total_recur += $recur;
+        $taxable_charged += $setup
+          unless $part_pkg->setuptax =~ /^Y$/i;
+        $taxable_charged += $recur
+          unless $part_pkg->recurtax =~ /^Y$/i;
+          
+        unless ( $self->tax =~ /Y/i
+                 || $self->payby eq 'COMP'
+                 || $taxable_charged == 0 ) {
+
+          my $cust_main_county = qsearchs('cust_main_county',{
+              'state'    => $self->state,
+              'county'   => $self->county,
+              'country'  => $self->country,
+              'taxclass' => $part_pkg->taxclass,
+          } );
+          $cust_main_county ||= qsearchs('cust_main_county',{
+              'state'    => $self->state,
+              'county'   => $self->county,
+              'country'  => $self->country,
+              'taxclass' => '',
+          } );
+          unless ( $cust_main_county ) {
+            $dbh->rollback if $oldAutoCommit;
+            return
+              "fatal: can't find tax rate for state/county/country/taxclass ".
+              join('/', ( map $self->$_(), qw(state county country) ),
+                        $part_pkg->taxclass ).  "\n";
+          }
+
+          if ( $cust_main_county->exempt_amount ) {
+            my ($mon,$year) = (localtime($sdate) )[4,5];
+            $mon++;
+            my $freq = $part_pkg->freq || 1;
+            my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
+            foreach my $which_month ( 1 .. $freq ) {
+              my %hash = (
+                'custnum' => $self->custnum,
+                'taxnum'  => $cust_main_county->taxnum,
+                'year'    => 1900+$year,
+                'month'   => $mon++,
+              );
+              #until ( $mon < 12 ) { $mon -= 12; $year++; }
+              until ( $mon < 13 ) { $mon -= 12; $year++; }
+              my $cust_tax_exempt =
+                qsearchs('cust_tax_exempt', \%hash)
+                || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
+              my $remaining_exemption = sprintf("%.2f",
+                $cust_main_county->exempt_amount - $cust_tax_exempt->amount );
+              if ( $remaining_exemption > 0 ) {
+                my $addl = $remaining_exemption > $taxable_per_month
+                  ? $taxable_per_month
+                  : $remaining_exemption;
+                $taxable_charged -= $addl;
+                my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
+                  $cust_tax_exempt->hash,
+                  'amount' => sprintf("%.2f", $cust_tax_exempt->amount + $addl),
+                } );
+                $error = $new_cust_tax_exempt->exemptnum
+                  ? $new_cust_tax_exempt->replace($cust_tax_exempt)
+                  : $new_cust_tax_exempt->insert;
+                if ( $error ) {
+                  $dbh->rollback if $oldAutoCommit;
+                  return "fatal: can't update cust_tax_exempt: $error";
+                }
+
+              } # if $remaining_exemption > 0
+
+            } #foreach $which_month
+
+          } #if $cust_main_county->exempt_amount
+
+          $taxable_charged = sprintf( "%.2f", $taxable_charged);
+
+          #$tax += $taxable_charged * $cust_main_county->tax / 100
+          $tax{ $cust_main_county->taxname || 'Tax' } +=
+            $taxable_charged * $cust_main_county->tax / 100
+
+        } #unless $self->tax =~ /Y/i
+          #       || $self->payby eq 'COMP'
+          #       || $taxable_charged == 0
+
+      } #if $setup > 0 || $recur > 0
+      
+    } #if $cust_pkg_mod_flag
+
+  } #foreach my $cust_pkg
+
+  my $charged = sprintf( "%.2f", $total_setup + $total_recur );
+#  my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
+
+  unless ( @cust_bill_pkg ) { #don't create invoices with no line items
+    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    return '';
+  } 
+
+#  unless ( $self->tax =~ /Y/i
+#           || $self->payby eq 'COMP'
+#           || $taxable_charged == 0 ) {
+#    my $cust_main_county = qsearchs('cust_main_county',{
+#        'state'   => $self->state,
+#        'county'  => $self->county,
+#        'country' => $self->country,
+#    } ) or die "fatal: can't find tax rate for state/county/country ".
+#               $self->state. "/". $self->county. "/". $self->country. "\n";
+#    my $tax = sprintf( "%.2f",
+#      $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
+#    );
+
+  foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
+    my $tax = sprintf("%.2f", $tax{$taxname} );
+    $charged = sprintf( "%.2f", $charged+$tax );
+
+    my $cust_bill_pkg = new FS::cust_bill_pkg ({
+      'pkgnum'   => 0,
+      'setup'    => $tax,
+      'recur'    => 0,
+      'sdate'    => '',
+      'edate'    => '',
+      'itemdesc' => $taxname,
+    });
+    push @cust_bill_pkg, $cust_bill_pkg;
+  }
+#  }
+
+  my $cust_bill = new FS::cust_bill ( {
+    'custnum' => $self->custnum,
+    '_date'   => $time,
+    'charged' => $charged,
+  } );
+  $error = $cust_bill->insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "can't create invoice for customer #". $self->custnum. ": $error";
+  }
+
+  my $invnum = $cust_bill->invnum;
+  my $cust_bill_pkg;
+  foreach $cust_bill_pkg ( @cust_bill_pkg ) {
+    #warn $invnum;
+    $cust_bill_pkg->invnum($invnum);
+    $error = $cust_bill_pkg->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't create invoice line item for customer #". $self->custnum.
+             ": $error";
+    }
+  }
+  
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+}
+
+=item collect OPTIONS
+
+(Attempt to) collect money for this customer's outstanding invoices (see
+L<FS::cust_bill>).  Usually used after the bill method.
+
+Depending on the value of `payby', this may print or email an invoice (I<BILL>,
+I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
+check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
+
+Most actions are now triggered by invoice events; see L<FS::part_bill_event>
+and the invoice events web interface.
+
+If there is an error, returns the error, otherwise returns false.
+
+Options are passed as name-value pairs.
+
+Currently available options are:
+
+invoice_time - Use this time when deciding when to print invoices and
+late notices on those invoices.  The default is now.  It is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse>
+for conversion functions.
+
+retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
+events.
+
+retry_card - Deprecated alias for 'retry'
+
+batch_card - This option is deprecated.  See the invoice events web interface
+to control whether cards are batched or run against a realtime gateway.
+
+report_badcard - This option is deprecated.
+
+force_print - This option is deprecated; see the invoice events web interface.
+
+=cut
+
+sub collect {
+  my( $self, %options ) = @_;
+  my $invoice_time = $options{'invoice_time'} || time;
+
+  #put below somehow?
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $balance = $self->balance;
+  warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
+  unless ( $balance > 0 ) { #redundant?????
+    $dbh->rollback if $oldAutoCommit; #hmm
+    return '';
+  }
+
+  if ( exists($options{'retry_card'}) ) {
+    carp 'retry_card option passed to collect is deprecated; use retry';
+    $options{'retry'} ||= $options{'retry_card'};
+  }
+  if ( exists($options{'retry'}) && $options{'retry'} ) {
+    my $error = $self->retry_realtime;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  foreach my $cust_bill ( $self->cust_bill ) {
+
+    #this has to be before next's
+    my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
+                                  ? $balance
+                                  : $cust_bill->owed
+    );
+    $balance = sprintf( "%.2f", $balance - $amount );
+
+    next unless $cust_bill->owed > 0;
+
+    # don't try to charge for the same invoice if it's already in a batch
+    #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
+
+    warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
+
+    next unless $amount > 0;
+
+
+    foreach my $part_bill_event (
+      sort {    $a->seconds   <=> $b->seconds
+             || $a->weight    <=> $b->weight
+             || $a->eventpart <=> $b->eventpart }
+        grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
+               && ! qsearchs( 'cust_bill_event', {
+                                'invnum'    => $cust_bill->invnum,
+                                'eventpart' => $_->eventpart,
+                                'status'    => 'done',
+                                                                   } )
+             }
+          qsearch('part_bill_event', { 'payby'    => $self->payby,
+                                       'disabled' => '',           } )
+    ) {
+
+      last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
+
+      warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
+        if $Debug;
+      my $cust_main = $self; #for callback
+      my $error = eval $part_bill_event->eventcode;
+
+      my $status = '';
+      my $statustext = '';
+      if ( $@ ) {
+        $status = 'failed';
+        $statustext = $@;
+      } elsif ( $error ) {
+        $status = 'done';
+        $statustext = $error;
+      } else {
+        $status = 'done'
+      }
+
+      #add cust_bill_event
+      my $cust_bill_event = new FS::cust_bill_event {
+        'invnum'     => $cust_bill->invnum,
+        'eventpart'  => $part_bill_event->eventpart,
+        #'_date'      => $invoice_time,
+        '_date'      => time,
+        'status'     => $status,
+        'statustext' => $statustext,
+      };
+      $error = $cust_bill_event->insert;
+      if ( $error ) {
+        #$dbh->rollback if $oldAutoCommit;
+        #return "error: $error";
+
+        # gah, even with transactions.
+        $dbh->commit if $oldAutoCommit; #well.
+        my $e = 'WARNING: Event run but database not updated - '.
+                'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
+                ', eventpart '. $part_bill_event->eventpart.
+                ": $error";
+        warn $e;
+        return $e;
+      }
+
+
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item retry_realtime
+
+Schedules realtime credit card / electronic check / LEC billing events for
+for retry.  Useful if card information has changed or manual retry is desired.
+The 'collect' method must be called to actually retry the transaction.
+
+Implementation details: For each of this customer's open invoices, changes
+the status of the first "done" (with statustext error) realtime processing
+event to "failed".
+
+=cut
+
+sub retry_realtime {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_bill (
+    grep { $_->cust_bill_event }
+      $self->open_cust_bill
+  ) {
+    my @cust_bill_event =
+      sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
+        grep {
+               #$_->part_bill_event->plan eq 'realtime-card'
+               $_->part_bill_event->eventcode =~
+                   /\$cust_bill\->realtime_(card|ach|lec)$/
+                 && $_->status eq 'done'
+                 && $_->statustext
+             }
+          $cust_bill->cust_bill_event;
+    next unless @cust_bill_event;
+    my $error = $cust_bill_event[0]->retry;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "error scheduling invoice event for retry: $error";
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
+
+Runs a realtime credit card, ACH (electronic check) or phone bill transaction
+via a Business::OnlinePayment realtime gateway.  See
+L<http://420.am/business-onlinepayment> for supported gateways.
+
+Available methods are: I<CC>, I<ECHECK> and I<LEC>
+
+Available options are: I<description>, I<invnum>, I<quiet>
+
+The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
+I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
+if set, will override the value from the customer record.
+
+I<description> is a free-text field passed to the gateway.  It defaults to
+"Internet services".
+
+If an I<invnum> is specified, this payment (if sucessful) is applied to the
+specified invoice.  If you don't specify an I<invnum> you might want to
+call the B<apply_payments> method.
+
+I<quiet> can be set true to surpress email decline notices.
+
+(moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
+
+=cut
+
+sub realtime_bop {
+  my( $self, $method, $amount, %options ) = @_;
+  if ( $Debug ) {
+    warn "$self $method $amount\n";
+    warn "  $_ => $options{$_}\n" foreach keys %options;
+  }
+
+  $options{'description'} ||= 'Internet services';
+
+  #pre-requisites
+  die "Real-time processing not enabled\n"
+    unless $conf->exists('business-onlinepayment');
+  eval "use Business::OnlinePayment";  
+  die $@ if $@;
+
+  #overrides
+  $self->set( $_ => $options{$_} )
+    foreach grep { exists($options{$_}) }
+            qw( payname address1 address2 city state zip payinfo paydate );
+
+  #load up config
+  my $bop_config = 'business-onlinepayment';
+  $bop_config .= '-ach'
+    if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
+  my ( $processor, $login, $password, $action, @bop_options ) =
+    $conf->config($bop_config);
+  $action ||= 'normal authorization';
+  pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
+
+  #massage data
+
+  my $address = $self->address1;
+  $address .= ", ". $self->address2 if $self->address2;
+
+  my($payname, $payfirst, $paylast);
+  if ( $self->payname && $method ne 'ECHECK' ) {
+    $payname = $self->payname;
+    $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
+      or return "Illegal payname $payname";
+    ($payfirst, $paylast) = ($1, $2);
+  } else {
+    $payfirst = $self->getfield('first');
+    $paylast = $self->getfield('last');
+    $payname =  "$payfirst $paylast";
+  }
+
+  my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
+  if ( $conf->exists('emailinvoiceauto')
+       || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
+    push @invoicing_list, $self->all_emails;
+  }
+  my $email = $invoicing_list[0];
+
+  my %content;
+  if ( $method eq 'CC' ) { 
+    $content{card_number} = $self->payinfo;
+    $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+    $content{expiration} = "$2/$1";
+  } elsif ( $method eq 'ECHECK' ) {
+    my($account_number,$routing_code) = $self->payinfo;
+    ( $content{account_number}, $content{routing_code} ) =
+      split('@', $self->payinfo);
+    $content{bank_name} = $self->payname;
+    $content{account_type} = 'CHECKING';
+    $content{account_name} = $payname;
+    $content{customer_org} = $self->company ? 'B' : 'I';
+    $content{customer_ssn} = $self->ss;
+  } elsif ( $method eq 'LEC' ) {
+    $content{phone} = $self->payinfo;
+  }
+
+  #transaction(s)
+
+  my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
+
+  my $transaction =
+    new Business::OnlinePayment( $processor, @bop_options );
+  $transaction->content(
+    'type'           => $method,
+    'login'          => $login,
+    'password'       => $password,
+    'action'         => $action1,
+    'description'    => $options{'description'},
+    'amount'         => $amount,
+    'invoice_number' => $options{'invnum'},
+    'customer_id'    => $self->custnum,
+    'last_name'      => $paylast,
+    'first_name'     => $payfirst,
+    'name'           => $payname,
+    'address'        => $address,
+    'city'           => $self->city,
+    'state'          => $self->state,
+    'zip'            => $self->zip,
+    'country'        => $self->country,
+    'referer'        => 'http://cleanwhisker.420.am/',
+    'email'          => $email,
+    'phone'          => $self->daytime || $self->night,
+    %content, #after
+  );
+  $transaction->submit();
+
+  if ( $transaction->is_success() && $action2 ) {
+    my $auth = $transaction->authorization;
+    my $ordernum = $transaction->can('order_number')
+                   ? $transaction->order_number
+                   : '';
+
+    my $capture =
+      new Business::OnlinePayment( $processor, @bop_options );
+
+    my %capture = (
+      %content,
+      type           => $method,
+      action         => $action2,
+      login          => $login,
+      password       => $password,
+      order_number   => $ordernum,
+      amount         => $amount,
+      authorization  => $auth,
+      description    => $options{'description'},
+    );
+
+    foreach my $field (qw( authorization_source_code returned_ACI                                          transaction_identifier validation_code           
+                           transaction_sequence_num local_transaction_date    
+                           local_transaction_time AVS_result_code          )) {
+      $capture{$field} = $transaction->$field() if $transaction->can($field);
+    }
+
+    $capture->content( %capture );
+
+    $capture->submit();
+
+    unless ( $capture->is_success ) {
+      my $e = "Authorization sucessful but capture failed, custnum #".
+              $self->custnum. ': '.  $capture->result_code.
+              ": ". $capture->error_message;
+      warn $e;
+      return $e;
+    }
+
+  }
+
+  #result handling
+  if ( $transaction->is_success() ) {
+
+    my %method2payby = (
+      'CC'     => 'CARD',
+      'ECHECK' => 'CHEK',
+      'LEC'    => 'LECB',
+    );
+
+    my $cust_pay = new FS::cust_pay ( {
+       'custnum'  => $self->custnum,
+       'invnum'   => $options{'invnum'},
+       'paid'     => $amount,
+       '_date'     => '',
+       'payby'    => $method2payby{$method},
+       'payinfo'  => $self->payinfo,
+       'paybatch' => "$processor:". $transaction->authorization,
+    } );
+    my $error = $cust_pay->insert;
+    if ( $error ) {
+      # gah, even with transactions.
+      my $e = 'WARNING: Card/ACH debited but database not updated - '.
+              'error applying payment, invnum #' . $self->invnum.
+              " ($processor): $error";
+      warn $e;
+      return $e;
+    } else {
+      return '';
+    }
+
+  } else {
+
+    my $perror = "$processor error: ". $transaction->error_message;
+
+    if ( !$options{'quiet'} && $conf->exists('emaildecline')
+         && grep { $_ ne 'POST' } $self->invoicing_list
+    ) {
+      my @templ = $conf->config('declinetemplate');
+      my $template = new Text::Template (
+        TYPE   => 'ARRAY',
+        SOURCE => [ map "$_\n", @templ ],
+      ) or return "($perror) can't create template: $Text::Template::ERROR";
+      $template->compile()
+        or return "($perror) can't compile template: $Text::Template::ERROR";
+
+      my $templ_hash = { error => $transaction->error_message };
+
+      my $error = send_email(
+        'from'    => $conf->config('invoice_from'),
+        'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
+        'subject' => 'Your payment could not be processed',
+        'body'    => [ $template->fill_in(HASH => $templ_hash) ],
+      );
+
+      $perror .= " (also received error sending decline notification: $error)"
+        if $error;
+
+    }
+  
+    return $perror;
+  }
+
+}
+
+=item total_owed
+
+Returns the total owed for this customer on all invoices
+(see L<FS::cust_bill/owed>).
+
+=cut
+
+sub total_owed {
+  my $self = shift;
+  $self->total_owed_date(2145859200); #12/31/2037
+}
+
+=item total_owed_date TIME
+
+Returns the total owed for this customer on all invoices with date earlier than
+TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
+see L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub total_owed_date {
+  my $self = shift;
+  my $time = shift;
+  my $total_bill = 0;
+  foreach my $cust_bill (
+    grep { $_->_date <= $time }
+      qsearch('cust_bill', { 'custnum' => $self->custnum, } )
+  ) {
+    $total_bill += $cust_bill->owed;
+  }
+  sprintf( "%.2f", $total_bill );
+}
+
+=item apply_credits
+
+Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
+to outstanding invoice balances in chronological order and returns the value
+of any remaining unapplied credits available for refund
+(see L<FS::cust_refund>).
+
+=cut
+
+sub apply_credits {
+  my $self = shift;
+
+  return 0 unless $self->total_credited;
+
+  my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
+      qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
+
+  my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
+      qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
+
+  my $credit;
+
+  foreach my $cust_bill ( @invoices ) {
+    my $amount;
+
+    if ( !defined($credit) || $credit->credited == 0) {
+      $credit = pop @credits or last;
+    }
+
+    if ($cust_bill->owed >= $credit->credited) {
+      $amount=$credit->credited;
+    }else{
+      $amount=$cust_bill->owed;
+    }
+    
+    my $cust_credit_bill = new FS::cust_credit_bill ( {
+      'crednum' => $credit->crednum,
+      'invnum'  => $cust_bill->invnum,
+      'amount'  => $amount,
+    } );
+    my $error = $cust_credit_bill->insert;
+    die $error if $error;
+    
+    redo if ($cust_bill->owed > 0);
+
+  }
+
+  return $self->total_credited;
+}
+
+=item apply_payments
+
+Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
+to outstanding invoice balances in chronological order.
+
+ #and returns the value of any remaining unapplied payments.
+
+=cut
+
+sub apply_payments {
+  my $self = shift;
+
+  #return 0 unless
+
+  my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
+      qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
+
+  my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
+      qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
+
+  my $payment;
+
+  foreach my $cust_bill ( @invoices ) {
+    my $amount;
+
+    if ( !defined($payment) || $payment->unapplied == 0 ) {
+      $payment = pop @payments or last;
+    }
+
+    if ( $cust_bill->owed >= $payment->unapplied ) {
+      $amount = $payment->unapplied;
+    } else {
+      $amount = $cust_bill->owed;
+    }
+
+    my $cust_bill_pay = new FS::cust_bill_pay ( {
+      'paynum' => $payment->paynum,
+      'invnum' => $cust_bill->invnum,
+      'amount' => $amount,
+    } );
+    my $error = $cust_bill_pay->insert;
+    die $error if $error;
+
+    redo if ( $cust_bill->owed > 0);
+
+  }
+
+  return $self->total_unapplied_payments;
+}
+
+=item total_credited
+
+Returns the total outstanding credit (see L<FS::cust_credit>) for this
+customer.  See L<FS::cust_credit/credited>.
+
+=cut
+
+sub total_credited {
+  my $self = shift;
+  my $total_credit = 0;
+  foreach my $cust_credit ( qsearch('cust_credit', {
+    'custnum' => $self->custnum,
+  } ) ) {
+    $total_credit += $cust_credit->credited;
+  }
+  sprintf( "%.2f", $total_credit );
+}
+
+=item total_unapplied_payments
+
+Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
+See L<FS::cust_pay/unapplied>.
+
+=cut
+
+sub total_unapplied_payments {
+  my $self = shift;
+  my $total_unapplied = 0;
+  foreach my $cust_pay ( qsearch('cust_pay', {
+    'custnum' => $self->custnum,
+  } ) ) {
+    $total_unapplied += $cust_pay->unapplied;
+  }
+  sprintf( "%.2f", $total_unapplied );
+}
+
+=item balance
+
+Returns the balance for this customer (total_owed minus total_credited
+minus total_unapplied_payments).
+
+=cut
+
+sub balance {
+  my $self = shift;
+  sprintf( "%.2f",
+    $self->total_owed - $self->total_credited - $self->total_unapplied_payments
+  );
+}
+
+=item balance_date TIME
+
+Returns the balance for this customer, only considering invoices with date
+earlier than TIME (total_owed_date minus total_credited minus
+total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
+L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
+functions.
+
+=cut
+
+sub balance_date {
+  my $self = shift;
+  my $time = shift;
+  sprintf( "%.2f",
+    $self->total_owed_date($time)
+      - $self->total_credited
+      - $self->total_unapplied_payments
+  );
+}
+
+=item invoicing_list [ ARRAYREF ]
+
+If an arguement is given, sets these email addresses as invoice recipients
+(see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
+(except as warnings), so use check_invoicing_list first.
+
+Returns a list of email addresses (with svcnum entries expanded).
+
+Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
+check it without disturbing anything by passing nothing.
+
+This interface may change in the future.
+
+=cut
+
+sub invoicing_list {
+  my( $self, $arrayref ) = @_;
+  if ( $arrayref ) {
+    my @cust_main_invoice;
+    if ( $self->custnum ) {
+      @cust_main_invoice = 
+        qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
+    } else {
+      @cust_main_invoice = ();
+    }
+    foreach my $cust_main_invoice ( @cust_main_invoice ) {
+      #warn $cust_main_invoice->destnum;
+      unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
+        #warn $cust_main_invoice->destnum;
+        my $error = $cust_main_invoice->delete;
+        warn $error if $error;
+      }
+    }
+    if ( $self->custnum ) {
+      @cust_main_invoice = 
+        qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
+    } else {
+      @cust_main_invoice = ();
+    }
+    my %seen = map { $_->address => 1 } @cust_main_invoice;
+    foreach my $address ( @{$arrayref} ) {
+      next if exists $seen{$address} && $seen{$address};
+      $seen{$address} = 1;
+      my $cust_main_invoice = new FS::cust_main_invoice ( {
+        'custnum' => $self->custnum,
+        'dest'    => $address,
+      } );
+      my $error = $cust_main_invoice->insert;
+      warn $error if $error;
+    }
+  }
+  if ( $self->custnum ) {
+    map { $_->address }
+      qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
+  } else {
+    ();
+  }
+}
+
+=item check_invoicing_list ARRAYREF
+
+Checks these arguements as valid input for the invoicing_list method.  If there
+is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub check_invoicing_list {
+  my( $self, $arrayref ) = @_;
+  foreach my $address ( @{$arrayref} ) {
+    my $cust_main_invoice = new FS::cust_main_invoice ( {
+      'custnum' => $self->custnum,
+      'dest'    => $address,
+    } );
+    my $error = $self->custnum
+                ? $cust_main_invoice->check
+                : $cust_main_invoice->checkdest
+    ;
+    return $error if $error;
+  }
+  '';
+}
+
+=item set_default_invoicing_list
+
+Sets the invoicing list to all accounts associated with this customer,
+overwriting any previous invoicing list.
+
+=cut
+
+sub set_default_invoicing_list {
+  my $self = shift;
+  $self->invoicing_list($self->all_emails);
+}
+
+=item all_emails
+
+Returns the email addresses of all accounts provisioned for this customer.
+
+=cut
+
+sub all_emails {
+  my $self = shift;
+  my %list;
+  foreach my $cust_pkg ( $self->all_pkgs ) {
+    my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
+    my @svc_acct =
+      map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
+        grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
+          @cust_svc;
+    $list{$_}=1 foreach map { $_->email } @svc_acct;
+  }
+  keys %list;
+}
+
+=item invoicing_list_addpost
+
+Adds postal invoicing to this customer.  If this customer is already configured
+to receive postal invoices, does nothing.
+
+=cut
+
+sub invoicing_list_addpost {
+  my $self = shift;
+  return if grep { $_ eq 'POST' } $self->invoicing_list;
+  my @invoicing_list = $self->invoicing_list;
+  push @invoicing_list, 'POST';
+  $self->invoicing_list(\@invoicing_list);
+}
+
+=item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
+
+Returns an array of customers referred by this customer (referral_custnum set
+to this custnum).  If DEPTH is given, recurses up to the given depth, returning
+customers referred by customers referred by this customer and so on, inclusive.
+The default behavior is DEPTH 1 (no recursion).
+
+=cut
+
+sub referral_cust_main {
+  my $self = shift;
+  my $depth = @_ ? shift : 1;
+  my $exclude = @_ ? shift : {};
+
+  my @cust_main =
+    map { $exclude->{$_->custnum}++; $_; }
+      grep { ! $exclude->{ $_->custnum } }
+        qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
+
+  if ( $depth > 1 ) {
+    push @cust_main,
+      map { $_->referral_cust_main($depth-1, $exclude) }
+        @cust_main;
+  }
+
+  @cust_main;
+}
+
+=item referral_cust_main_ncancelled
+
+Same as referral_cust_main, except only returns customers with uncancelled
+packages.
+
+=cut
+
+sub referral_cust_main_ncancelled {
+  my $self = shift;
+  grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
+}
+
+=item referral_cust_pkg [ DEPTH ]
+
+Like referral_cust_main, except returns a flat list of all unsuspended (and
+uncancelled) packages for each customer.  The number of items in this list may
+be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
+
+=cut
+
+sub referral_cust_pkg {
+  my $self = shift;
+  my $depth = @_ ? shift : 1;
+
+  map { $_->unsuspended_pkgs }
+    grep { $_->unsuspended_pkgs }
+      $self->referral_cust_main($depth);
+}
+
+=item credit AMOUNT, REASON
+
+Applies a credit to this customer.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub credit {
+  my( $self, $amount, $reason ) = @_;
+  my $cust_credit = new FS::cust_credit {
+    'custnum' => $self->custnum,
+    'amount'  => $amount,
+    'reason'  => $reason,
+  };
+  $cust_credit->insert;
+}
+
+=item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
+
+Creates a one-time charge for this customer.  If there is an error, returns
+the error, otherwise returns false.
+
+=cut
+
+sub charge {
+  my ( $self, $amount ) = ( shift, shift );
+  my $pkg      = @_ ? shift : 'One-time charge';
+  my $comment  = @_ ? shift : '$'. sprintf("%.2f",$amount);
+  my $taxclass = @_ ? shift : '';
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $part_pkg = new FS::part_pkg ( {
+    'pkg'      => $pkg,
+    'comment'  => $comment,
+    'setup'    => $amount,
+    'freq'     => 0,
+    'recur'    => '0',
+    'disabled' => 'Y',
+    'taxclass' => $taxclass,
+  } );
+
+  my $error = $part_pkg->insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  my $pkgpart = $part_pkg->pkgpart;
+  my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
+  unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
+    my $type_pkgs = new FS::type_pkgs \%type_pkgs;
+    $error = $type_pkgs->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  my $cust_pkg = new FS::cust_pkg ( {
+    'custnum' => $self->custnum,
+    'pkgpart' => $pkgpart,
+  } );
+
+  $error = $cust_pkg->insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item cust_bill
+
+Returns all the invoices (see L<FS::cust_bill>) for this customer.
+
+=cut
+
+sub cust_bill {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch('cust_bill', { 'custnum' => $self->custnum, } )
+}
+
+=item open_cust_bill
+
+Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
+customer.
+
+=cut
+
+sub open_cust_bill {
+  my $self = shift;
+  grep { $_->owed > 0 } $self->cust_bill;
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item check_and_rebuild_fuzzyfiles
+
+=cut
+
+sub check_and_rebuild_fuzzyfiles {
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+  -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
+    or &rebuild_fuzzyfiles;
+}
+
+=item rebuild_fuzzyfiles
+
+=cut
+
+sub rebuild_fuzzyfiles {
+
+  use Fcntl qw(:flock);
+
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+
+  #last
+
+  open(LASTLOCK,">>$dir/cust_main.last")
+    or die "can't open $dir/cust_main.last: $!";
+  flock(LASTLOCK,LOCK_EX)
+    or die "can't lock $dir/cust_main.last: $!";
+
+  my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
+  push @all_last,
+                 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
+    if defined dbdef->table('cust_main')->column('ship_last');
+
+  open (LASTCACHE,">$dir/cust_main.last.tmp")
+    or die "can't open $dir/cust_main.last.tmp: $!";
+  print LASTCACHE join("\n", @all_last), "\n";
+  close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
+
+  rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
+  close LASTLOCK;
+
+  #company
+
+  open(COMPANYLOCK,">>$dir/cust_main.company")
+    or die "can't open $dir/cust_main.company: $!";
+  flock(COMPANYLOCK,LOCK_EX)
+    or die "can't lock $dir/cust_main.company: $!";
+
+  my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
+  push @all_company,
+       grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
+    if defined dbdef->table('cust_main')->column('ship_last');
+
+  open (COMPANYCACHE,">$dir/cust_main.company.tmp")
+    or die "can't open $dir/cust_main.company.tmp: $!";
+  print COMPANYCACHE join("\n", @all_company), "\n";
+  close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
+
+  rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
+  close COMPANYLOCK;
+
+}
+
+=item all_last
+
+=cut
+
+sub all_last {
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+  open(LASTCACHE,"<$dir/cust_main.last")
+    or die "can't open $dir/cust_main.last: $!";
+  my @array = map { chomp; $_; } <LASTCACHE>;
+  close LASTCACHE;
+  \@array;
+}
+
+=item all_company
+
+=cut
+
+sub all_company {
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+  open(COMPANYCACHE,"<$dir/cust_main.company")
+    or die "can't open $dir/cust_main.last: $!";
+  my @array = map { chomp; $_; } <COMPANYCACHE>;
+  close COMPANYCACHE;
+  \@array;
+}
+
+=item append_fuzzyfiles LASTNAME COMPANY
+
+=cut
+
+sub append_fuzzyfiles {
+  my( $last, $company ) = @_;
+
+  &check_and_rebuild_fuzzyfiles;
+
+  use Fcntl qw(:flock);
+
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+
+  if ( $last ) {
+
+    open(LAST,">>$dir/cust_main.last")
+      or die "can't open $dir/cust_main.last: $!";
+    flock(LAST,LOCK_EX)
+      or die "can't lock $dir/cust_main.last: $!";
+
+    print LAST "$last\n";
+
+    flock(LAST,LOCK_UN)
+      or die "can't unlock $dir/cust_main.last: $!";
+    close LAST;
+  }
+
+  if ( $company ) {
+
+    open(COMPANY,">>$dir/cust_main.company")
+      or die "can't open $dir/cust_main.company: $!";
+    flock(COMPANY,LOCK_EX)
+      or die "can't lock $dir/cust_main.company: $!";
+
+    print COMPANY "$company\n";
+
+    flock(COMPANY,LOCK_UN)
+      or die "can't unlock $dir/cust_main.company: $!";
+
+    close COMPANY;
+  }
+
+  1;
+}
+
+=item batch_import
+
+=cut
+
+sub batch_import {
+  my $param = shift;
+  #warn join('-',keys %$param);
+  my $fh = $param->{filehandle};
+  my $agentnum = $param->{agentnum};
+  my $refnum = $param->{refnum};
+  my $pkgpart = $param->{pkgpart};
+  my @fields = @{$param->{fields}};
+
+  eval "use Date::Parse;";
+  die $@ if $@;
+  eval "use Text::CSV_XS;";
+  die $@ if $@;
+
+  my $csv = new Text::CSV_XS;
+  #warn $csv;
+  #warn $fh;
+
+  my $imported = 0;
+  #my $columns;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+  
+  #while ( $columns = $csv->getline($fh) ) {
+  my $line;
+  while ( defined($line=<$fh>) ) {
+
+    $csv->parse($line) or do {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't parse: ". $csv->error_input();
+    };
+
+    my @columns = $csv->fields();
+    #warn join('-',@columns);
+
+    my %cust_main = (
+      agentnum => $agentnum,
+      refnum   => $refnum,
+      country  => 'US', #default
+      payby    => 'BILL', #default
+      paydate  => '12/2037', #default
+    );
+    my $billtime = time;
+    my %cust_pkg = ( pkgpart => $pkgpart );
+    foreach my $field ( @fields ) {
+      if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
+        #$cust_pkg{$1} = str2time( shift @$columns );
+        if ( $1 eq 'setup' ) {
+          $billtime = str2time(shift @columns);
+        } else {
+          $cust_pkg{$1} = str2time( shift @columns );
+        }
+      } else {
+        #$cust_main{$field} = shift @$columns; 
+        $cust_main{$field} = shift @columns; 
+      }
+    }
+
+    my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
+    my $cust_main = new FS::cust_main ( \%cust_main );
+    use Tie::RefHash;
+    tie my %hash, 'Tie::RefHash'; #this part is important
+    $hash{$cust_pkg} = [] if $pkgpart;
+    my $error = $cust_main->insert( \%hash );
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't insert customer for $line: $error";
+    }
+
+    #false laziness w/bill.cgi
+    $error = $cust_main->bill( 'time' => $billtime );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't bill customer for $line: $error";
+    }
+
+    $cust_main->apply_payments;
+    $cust_main->apply_credits;
+
+    $error = $cust_main->collect();
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't collect customer for $line: $error";
+    }
+
+    $imported++;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  return "Empty file!" unless $imported;
+
+  ''; #no error
+
+}
+
+=item batch_charge
+
+=cut
+
+sub batch_charge {
+  my $param = shift;
+  #warn join('-',keys %$param);
+  my $fh = $param->{filehandle};
+  my @fields = @{$param->{fields}};
+
+  eval "use Date::Parse;";
+  die $@ if $@;
+  eval "use Text::CSV_XS;";
+  die $@ if $@;
+
+  my $csv = new Text::CSV_XS;
+  #warn $csv;
+  #warn $fh;
+
+  my $imported = 0;
+  #my $columns;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+  
+  #while ( $columns = $csv->getline($fh) ) {
+  my $line;
+  while ( defined($line=<$fh>) ) {
+
+    $csv->parse($line) or do {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't parse: ". $csv->error_input();
+    };
+
+    my @columns = $csv->fields();
+    #warn join('-',@columns);
+
+    my %row = ();
+    foreach my $field ( @fields ) {
+      $row{$field} = shift @columns;
+    }
+
+    my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
+    unless ( $cust_main ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "unknown custnum $row{'custnum'}";
+    }
+
+    if ( $row{'amount'} > 0 ) {
+      my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+      $imported++;
+    } elsif ( $row{'amount'} < 0 ) {
+      my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
+                                      $row{'pkg'}                         );
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+      $imported++;
+    } else {
+      #hmm?
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  return "Empty file!" unless $imported;
+
+  ''; #no error
+
+}
+
+=back
+
+=head1 BUGS
+
+The delete method.
+
+The delete method should possibly take an FS::cust_main object reference
+instead of a scalar customer number.
+
+Bill and collect options should probably be passed as references instead of a
+list.
+
+There should probably be a configuration file with a list of allowed credit
+card types.
+
+No multiple currency support (probably a larger project than just this module).
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
+L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
+L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm
new file mode 100644 (file)
index 0000000..d8796e4
--- /dev/null
@@ -0,0 +1,256 @@
+package FS::cust_main_county;
+
+use strict;
+use vars qw( @ISA @EXPORT_OK $conf
+             @cust_main_county %cust_main_county $countyflag );
+use Exporter;
+use FS::Record qw( qsearch );
+
+@ISA = qw( FS::Record );
+@EXPORT_OK = qw( regionselector );
+
+@cust_main_county = ();
+$countyflag = '';
+
+#ask FS::UID to run this stuff for us later
+$FS::UID::callback{'FS::cust_main_county'} = sub { 
+  $conf = new FS::Conf;
+};
+
+=head1 NAME
+
+FS::cust_main_county - Object methods for cust_main_county objects
+
+=head1 SYNOPSIS
+
+  use FS::cust_main_county;
+
+  $record = new FS::cust_main_county \%hash;
+  $record = new FS::cust_main_county { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  ($county_html, $state_html, $country_html) =
+    FS::cust_main_county::regionselector( $county, $state, $country );
+
+=head1 DESCRIPTION
+
+An FS::cust_main_county object represents a tax rate, defined by locale.
+FS::cust_main_county inherits from FS::Record.  The following fields are
+currently supported:
+
+=over 4
+
+=item taxnum - primary key (assigned automatically for new tax rates)
+
+=item state
+
+=item county
+
+=item country
+
+=item tax - percentage
+
+=item taxclass
+
+=item exempt_amount
+
+=item taxname - if defined, printed on invoices instead of "Tax"
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new tax rate.  To add the tax rate to the database, see L<"insert">.
+
+=cut
+
+sub table { 'cust_main_county'; }
+
+=item insert
+
+Adds this tax rate to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this tax rate from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid tax rate.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  $self->exempt_amount(0) unless $self->exempt_amount;
+
+  $self->ut_numbern('taxnum')
+    || $self->ut_textn('state')
+    || $self->ut_textn('county')
+    || $self->ut_text('country')
+    || $self->ut_float('tax')
+    || $self->ut_textn('taxclass') # ...
+    || $self->ut_money('exempt_amount')
+    || $self->ut_textn('taxname')
+  ;
+
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item regionselector [ COUNTY STATE COUNTRY [ PREFIX [ ONCHANGE ] ] ]
+
+=cut
+
+sub regionselector {
+  my ( $selected_county, $selected_state, $selected_country,
+       $prefix, $onchange ) = @_;
+
+  $prefix = '' unless defined $prefix;
+
+  $countyflag = 0;
+
+#  unless ( @cust_main_county ) { #cache 
+    @cust_main_county = qsearch('cust_main_county', {} );
+    foreach my $c ( @cust_main_county ) {
+      $countyflag=1 if $c->county;
+      #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
+      $cust_main_county{$c->country}{$c->state}{$c->county} = 1;
+    }
+#  }
+  $countyflag=1 if $selected_county;
+
+  my $script_html = <<END;
+    <SCRIPT>
+    function opt(what,value,text) {
+      var optionName = new Option(text, value, false, false);
+      var length = what.length;
+      what.options[length] = optionName;
+    }
+    function ${prefix}country_changed(what) {
+      country = what.options[what.selectedIndex].text;
+      for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
+          what.form.${prefix}state.options[i] = null;
+END
+      #what.form.${prefix}state.options[0] = new Option('', '', false, true);
+
+  foreach my $country ( sort keys %cust_main_county ) {
+    $script_html .= "\nif ( country == \"$country\" ) {\n";
+    foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
+      my $text = $state || '(n/a)';
+      $script_html .= qq!opt(what.form.${prefix}state, "$state", "$text");\n!;
+    }
+    $script_html .= "}\n";
+  }
+
+  $script_html .= <<END;
+    }
+    function ${prefix}state_changed(what) {
+END
+
+  if ( $countyflag ) {
+    $script_html .= <<END;
+      state = what.options[what.selectedIndex].text;
+      country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
+      for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
+          what.form.${prefix}county.options[i] = null;
+END
+
+    foreach my $country ( sort keys %cust_main_county ) {
+      $script_html .= "\nif ( country == \"$country\" ) {\n";
+      foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
+        $script_html .= "\nif ( state == \"$state\" ) {\n";
+          #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
+          foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
+            my $text = $county || '(n/a)';
+            $script_html .=
+              qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
+          }
+        $script_html .= "}\n";
+      }
+      $script_html .= "}\n";
+    }
+  }
+
+  $script_html .= <<END;
+    }
+    </SCRIPT>
+END
+
+  my $county_html = $script_html;
+  if ( $countyflag ) {
+    $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$onchange">!;
+    $county_html .= '</SELECT>';
+  } else {
+    $county_html .=
+      qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$selected_county">!;
+  }
+
+  my $state_html = qq!<SELECT NAME="${prefix}state" !.
+                   qq!onChange="${prefix}state_changed(this); $onchange">!;
+  foreach my $state ( sort keys %{ $cust_main_county{$selected_country} } ) {
+    my $text = $state || '(n/a)';
+    my $selected = $state eq $selected_state ? 'SELECTED' : '';
+    $state_html .= "\n<OPTION $selected VALUE=$state>$text</OPTION>"
+  }
+  $state_html .= '</SELECT>';
+
+  $state_html .= '</SELECT>';
+
+  my $country_html = qq!<SELECT NAME="${prefix}country" !.
+                     qq!onChange="${prefix}country_changed(this); $onchange">!;
+  my $countrydefault = $conf->config('countrydefault') || 'US';
+  foreach my $country (
+    sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
+      keys %cust_main_county
+  ) {
+    my $selected = $country eq $selected_country ? ' SELECTED' : '';
+    $country_html .= "\n<OPTION$selected>$country</OPTION>"
+  }
+  $country_html .= '</SELECT>';
+
+  ($county_html, $state_html, $country_html);
+
+}
+
+=back
+
+=head1 BUGS
+
+regionselector?  putting web ui components in here?  they should probably live
+somewhere else...
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_main_invoice.pm b/FS/FS/cust_main_invoice.pm
new file mode 100644 (file)
index 0000000..bcb1437
--- /dev/null
@@ -0,0 +1,177 @@
+package FS::cust_main_invoice;
+
+use strict;
+use vars qw(@ISA $conf);
+use Exporter;
+use FS::Record qw( qsearchs );
+use FS::Conf;
+use FS::cust_main;
+use FS::svc_acct;
+use FS::Msgcat qw(gettext);
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::cust_main_invoice - Object methods for cust_main_invoice records
+
+=head1 SYNOPSIS
+
+  use FS::cust_main_invoice;
+
+  $record = new FS::cust_main_invoice \%hash;
+  $record = new FS::cust_main_invoice { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $email_address = $record->address;
+
+=head1 DESCRIPTION
+
+An FS::cust_main_invoice object represents an invoice destination.  FS::cust_main_invoice inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item destnum - primary key
+
+=item custnum - customer (see L<FS::cust_main>)
+
+=item dest - Invoice destination: If numeric, a svcnum (see L<FS::svc_acct>), if string, a literal email address, or `POST' to enable mailing (the default if no cust_main_invoice records exist)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new invoice destination.  To add the invoice destination to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_main_invoice'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+
+  return "Can't change custnum!" unless $old->custnum == $new->custnum;
+
+  $new->SUPER::replace($old);
+}
+
+
+=item check
+
+Checks all fields to make sure this is a valid invoice destination.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and repalce methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = $self->ut_numbern('destnum')
+           || $self->ut_number('custnum')
+           || $self->checkdest;
+  ;
+  return $error if $error;
+
+  return "Unknown customer"
+    unless qsearchs('cust_main',{ 'custnum' => $self->custnum });
+
+  ''; #noerror
+}
+
+=item checkdest
+
+Checks the dest field only.
+
+#If it finds that the account ends in the
+#same domain configured as the B<domain> configuration file, it will change the
+#invoice destination from an email address to a service number (see
+#L<FS::svc_acct>).
+
+=cut
+
+sub checkdest { 
+  my $self = shift;
+
+  my $error = $self->ut_text('dest');
+  return $error if $error;
+
+  if ( $self->dest eq 'POST' ) {
+    #contemplate our navel
+  } elsif ( $self->dest =~ /^(\d+)$/ ) {
+    return "Unknown local account (specified by svcnum: ". $self->dest. ")"
+      unless qsearchs( 'svc_acct', { 'svcnum' => $self->dest } );
+  } elsif ( $self->dest =~ /^([\w\.\-\&\+]+)\@(([\w\.\-]+\.)+\w+)$/ ) {
+    my($user, $domain) = ($1, $2);
+    $self->dest("$1\@$2");
+  } else {
+    return gettext("illegal_email_invoice_address");
+  }
+
+  ''; #no error
+}
+
+=item address
+
+Returns the literal email address for this record (or `POST').
+
+=cut
+
+sub address {
+  my $self = shift;
+  if ( $self->dest =~ /^(\d+)$/ ) {
+    my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $1 } )
+      or return undef;
+    $svc_acct->email;
+  } else {
+    $self->dest;
+  }
+}
+
+=back
+
+=head1 VERSION
+
+$Id: cust_main_invoice.pm,v 1.13 2002-09-18 22:50:44 ivan Exp $
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_main>
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
new file mode 100644 (file)
index 0000000..55f2fc4
--- /dev/null
@@ -0,0 +1,407 @@
+package FS::cust_pay;
+
+use strict;
+use vars qw( @ISA $conf $unsuspendauto );
+use Date::Format;
+use Business::CreditCard;
+use FS::UID qw( dbh );
+use FS::Record qw( dbh qsearch qsearchs dbh );
+use FS::Misc qw(send_email);
+use FS::cust_bill;
+use FS::cust_bill_pay;
+use FS::cust_main;
+
+@ISA = qw( FS::Record );
+
+#ask FS::UID to run this stuff for us later
+FS::UID->install_callback( sub { 
+  $conf = new FS::Conf;
+  $unsuspendauto = $conf->exists('unsuspendauto');
+} );
+
+=head1 NAME
+
+FS::cust_pay - Object methods for cust_pay objects
+
+=head1 SYNOPSIS
+
+  use FS::cust_pay;
+
+  $record = new FS::cust_pay \%hash;
+  $record = new FS::cust_pay { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_pay object represents a payment; the transfer of money from a
+customer.  FS::cust_pay inherits from FS::Record.  The following fields are
+currently supported:
+
+=over 4
+
+=item paynum - primary key (assigned automatically for new payments)
+
+=item custnum - customer (see L<FS::cust_main>)
+
+=item paid - Amount of this payment
+
+=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=item payby - `CARD' (credit cards), `CHEK' (electronic check/ACH),
+`LECB' (phone bill billing), `BILL' (billing), or `COMP' (free)
+
+=item payinfo - card number, check #, or comp issuer (4-8 lowercase alphanumerics; think username), respectively
+
+=item paybatch - text field for tracking card processing
+
+=item closed - books closed flag, empty or `Y'
+
+=back
+
+=head1 METHODS
+
+=over 4 
+
+=item new HASHREF
+
+Creates a new payment.  To add the payment to the databse, see L<"insert">.
+
+=cut
+
+sub table { 'cust_pay'; }
+
+=item insert
+
+Adds this payment to the database.
+
+For backwards-compatibility and convenience, if the additional field invnum
+is defined, an FS::cust_bill_pay record for the full amount of the payment
+will be created.  In this case, custnum is optional.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  if ( $self->invnum ) {
+    my $cust_bill = qsearchs('cust_bill', { 'invnum' => $self->invnum } )
+      or do {
+        $dbh->rollback if $oldAutoCommit;
+        return "Unknown cust_bill.invnum: ". $self->invnum;
+      };
+    $self->custnum($cust_bill->custnum );
+  }
+
+  my $cust_main = qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+  my $old_balance = $cust_main->balance;
+
+  my $error = $self->check;
+  return $error if $error;
+
+  $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "error inserting $self: $error";
+  }
+
+  if ( $self->invnum ) {
+    my $cust_bill_pay = new FS::cust_bill_pay {
+      'invnum' => $self->invnum,
+      'paynum' => $self->paynum,
+      'amount' => $self->paid,
+      '_date'  => $self->_date,
+    };
+    $error = $cust_bill_pay->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "error inserting $cust_bill_pay: $error";
+    }
+  }
+
+  if ( $self->paybatch =~ /^webui-/ ) {
+    my @cust_pay = qsearch('cust_pay', {
+      'custnum' => $self->custnum,
+      'paybatch' => $self->paybatch,
+    } );
+    if ( scalar(@cust_pay) > 1 ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "a payment with webui token ". $self->paybatch. " already exists";
+    }
+  }
+
+  #false laziness w/ cust_credit::insert
+  if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) {
+    my @errors = $cust_main->unsuspend;
+    #return 
+    # side-fx with nested transactions?  upstack rolls back?
+    warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
+         join(' / ', @errors)
+      if @errors;
+  }
+  #eslaf
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+sub upgrade_replace { #1.3.x->1.4.x
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->check;
+  return $error if $error;
+
+  my %new = $self->hash;
+  my $new = FS::cust_pay->new(\%new);
+
+  if ( $self->invnum ) {
+    my $cust_bill_pay = new FS::cust_bill_pay {
+      'invnum' => $self->invnum,
+      'paynum' => $self->paynum,
+      'amount' => $self->paid,
+      '_date'  => $self->_date,
+    };
+    $error = $cust_bill_pay->insert;
+    if ( $error =~ 
+           /total cust_bill_pay.amount and cust_credit_bill.amount .* for invnum .* greater than cust_bill.charged/ ) {
+      #warn $error;
+      my $cust_bill = qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
+      $new->custnum($cust_bill->custnum);
+    } elsif ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    } else {
+      $new->custnum($cust_bill_pay->cust_bill->custnum);
+    }
+  } else {
+    die;
+  }
+
+  $error = $new->SUPER::replace($self);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+
+}
+
+=item delete
+
+Deletes this payment and all associated applications (see L<FS::cust_bill_pay>),
+unless the closed flag is set.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  return "Can't delete closed payment" if $self->closed =~ /^Y/i;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_bill_pay ( $self->cust_bill_pay ) {
+    my $error = $cust_bill_pay->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  my $error = $self->SUPER::delete(@_);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $conf->config('deletepayments') ne '' ) {
+
+    my $cust_main = qsearchs('cust_main',{ 'custnum' => $self->custnum });
+
+    my $error = send_email(
+      'from'    => $conf->config('invoice_from'), #??? well as good as any
+      'to'      => $conf->config('deletepayments'),
+      'subject' => 'FREESIDE NOTIFICATION: Payment deleted',
+      'body'    => [
+        "This is an automatic message from your Freeside installation\n",
+        "informing you that the following payment has been deleted:\n",
+        "\n",
+        'paynum: '. $self->paynum. "\n",
+        'custnum: '. $self->custnum.
+          " (". $cust_main->last. ", ". $cust_main->first. ")\n",
+        'paid: $'. sprintf("%.2f", $self->paid). "\n",
+        'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n",
+        'payby: '. $self->payby. "\n",
+        'payinfo: '. $self->payinfo. "\n",
+        'paybatch: '. $self->paybatch. "\n",
+      ],
+    );
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "can't send payment deletion notification: $error";
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+=item replace OLD_RECORD
+
+Currently unimplemented (accounting reasons).
+
+=cut
+
+sub replace {
+   return "Can't (yet?) modify cust_pay records!";
+}
+
+=item check
+
+Checks all fields to make sure this is a valid payment.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert method.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('paynum')
+    || $self->ut_numbern('custnum')
+    || $self->ut_money('paid')
+    || $self->ut_numbern('_date')
+    || $self->ut_textn('paybatch')
+    || $self->ut_enum('closed', [ '', 'Y' ])
+  ;
+  return $error if $error;
+
+  return "paid must be > 0 " if $self->paid <= 0;
+
+  return "unknown cust_main.custnum: ". $self->custnum
+    unless $self->invnum
+           || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+
+  $self->_date(time) unless $self->_date;
+
+  $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP)$/ or return "Illegal payby";
+  $self->payby($1);
+
+  #false laziness with cust_refund::check
+  if ( $self->payby eq 'CARD' ) {
+    my $payinfo = $self->payinfo;
+    $payinfo =~ s/\D//g;
+    $self->payinfo($payinfo);
+    if ( $self->payinfo ) {
+      $self->payinfo =~ /^(\d{13,16})$/
+        or return "Illegal (mistyped?) credit card number (payinfo)";
+      $self->payinfo($1);
+      validate($self->payinfo) or return "Illegal credit card number";
+      return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
+    } else {
+      $self->payinfo('N/A');
+    }
+
+  } else {
+    $error = $self->ut_textn('payinfo');
+    return $error if $error;
+  }
+
+  ''; #no error
+
+}
+
+=item cust_bill_pay
+
+Returns all applications to invoices (see L<FS::cust_bill_pay>) for this
+payment.
+
+=cut
+
+sub cust_bill_pay {
+  my $self = shift;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_bill_pay', { 'paynum' => $self->paynum } )
+  ;
+}
+
+=item unapplied
+
+Returns the amount of this payment that is still unapplied; which is
+paid minus all payment applications (see L<FS::cust_bill_pay>).
+
+=cut
+
+sub unapplied {
+  my $self = shift;
+  my $amount = $self->paid;
+  $amount -= $_->amount foreach ( $self->cust_bill_pay );
+  sprintf("%.2f", $amount );
+}
+
+=back
+
+=head1 VERSION
+
+$Id: cust_pay.pm,v 1.24 2003-05-19 12:00:44 ivan Exp $
+
+=head1 BUGS
+
+Delete and replace methods.
+
+=head1 SEE ALSO
+
+L<FS::cust_bill_pay>, L<FS::cust_bill>, L<FS::Record>, schema.html from the
+base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_pay_batch.pm b/FS/FS/cust_pay_batch.pm
new file mode 100644 (file)
index 0000000..c4427c3
--- /dev/null
@@ -0,0 +1,209 @@
+package FS::cust_pay_batch;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record;
+use Business::CreditCard;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::cust_pay_batch - Object methods for batch cards
+
+=head1 SYNOPSIS
+
+  use FS::cust_pay_batch;
+
+  $record = new FS::cust_pay_batch \%hash;
+  $record = new FS::cust_pay_batch { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_pay_batch object represents a credit card transaction ready to be
+batched (sent to a processor).  FS::cust_pay_batch inherits from FS::Record.  
+Typically called by the collect method of an FS::cust_main object.  The
+following fields are currently supported:
+
+=over 4
+
+=item paybatchnum - primary key (automatically assigned)
+
+=item cardnum
+
+=item exp - card expiration 
+
+=item amount 
+
+=item invnum - invoice
+
+=item custnum - customer 
+
+=item payname - name on card 
+
+=item first - name 
+
+=item last - name 
+
+=item address1 
+
+=item address2 
+
+=item city 
+
+=item state 
+
+=item zip 
+
+=item country 
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'cust_pay_batch'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item replace OLD_RECORD
+
+#inactive
+#
+#Replaces the OLD_RECORD with this one in the database.  If there is an error,
+#returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  return "Can't (yet?) replace batched transactions!";
+}
+
+=item check
+
+Checks all fields to make sure this is a valid transaction.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and repalce methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+      $self->ut_numbern('paybatchnum')
+    || $self->ut_numbern('trancode') #depriciated
+    || $self->ut_number('cardnum') 
+    || $self->ut_money('amount')
+    || $self->ut_number('invnum')
+    || $self->ut_number('custnum')
+    || $self->ut_text('address1')
+    || $self->ut_textn('address2')
+    || $self->ut_text('city')
+    || $self->ut_textn('state')
+  ;
+
+  return $error if $error;
+
+  $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
+  $self->setfield('last',$1);
+
+  $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
+  $self->first($1);
+
+  my $cardnum = $self->cardnum;
+  $cardnum =~ s/\D//g;
+  $cardnum =~ /^(\d{13,16})$/
+    or return "Illegal credit card number";
+  $cardnum = $1;
+  $self->cardnum($cardnum);
+  validate($cardnum) or return "Illegal credit card number";
+  return "Unknown card type" if cardtype($cardnum) eq "Unknown";
+
+  if ( $self->exp eq '' ) {
+    return "Expriation date required"; #unless 
+    $self->exp('');
+  } else {
+    if ( $self->exp =~ /^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/ ) {
+      $self->exp("$1-$2-$3");
+    } elsif ( $self->exp =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
+      if ( length($2) == 4 ) {
+        $self->exp("$2-$1-01");
+      } elsif ( $2 > 98 ) { #should pry change to check for "this year"
+        $self->exp("19$2-$1-01");
+      } else {
+        $self->exp("20$2-$1-01");
+      }
+    } else {
+      return "Illegal expiration date";
+    }
+  }
+
+  if ( $self->payname eq '' ) {
+    $self->payname( $self->first. " ". $self->getfield('last') );
+  } else {
+    $self->payname =~ /^([\w \,\.\-\']+)$/
+      or return "Illegal billing name";
+    $self->payname($1);
+  }
+
+  #$self->zip =~ /^\s*(\w[\w\-\s]{3,8}\w)\s*$/
+  #  or return "Illegal zip: ". $self->zip;
+  #$self->zip($1);
+
+  $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
+  $self->country($1);
+
+  $error = $self->ut_zip('zip', $self->country);
+  return $error if $error;
+
+  #check invnum, custnum, ?
+
+  ''; #no error
+}
+
+=back
+
+=head1 VERSION
+
+$Id: cust_pay_batch.pm,v 1.6 2002-02-22 23:08:11 ivan Exp $
+
+=head1 BUGS
+
+There should probably be a configuration file with a list of allowed credit
+card types.
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
new file mode 100644 (file)
index 0000000..9f20603
--- /dev/null
@@ -0,0 +1,815 @@
+package FS::cust_pkg;
+
+use strict;
+use vars qw(@ISA $disable_agentcheck);
+use vars qw( $quiet );
+use FS::UID qw( getotaker dbh );
+use FS::Record qw( qsearch qsearchs );
+use FS::Misc qw( send_email );
+use FS::cust_svc;
+use FS::part_pkg;
+use FS::cust_main;
+use FS::type_pkgs;
+use FS::pkg_svc;
+use FS::cust_bill_pkg;
+
+# need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
+# setup }
+# because they load configuraion by setting FS::UID::callback (see TODO)
+use FS::svc_acct;
+use FS::svc_domain;
+use FS::svc_www;
+use FS::svc_forward;
+
+# for sending cancel emails in sub cancel
+use FS::Conf;
+
+@ISA = qw( FS::Record );
+
+$disable_agentcheck = 0;
+
+sub _cache {
+  my $self = shift;
+  my ( $hashref, $cache ) = @_;
+  #if ( $hashref->{'pkgpart'} ) {
+  if ( $hashref->{'pkg'} ) {
+    # #@{ $self->{'_pkgnum'} } = ();
+    # my $subcache = $cache->subcache('pkgpart', 'part_pkg');
+    # $self->{'_pkgpart'} = $subcache;
+    # #push @{ $self->{'_pkgnum'} },
+    #   FS::part_pkg->new_or_cached($hashref, $subcache);
+    $self->{'_pkgpart'} = FS::part_pkg->new($hashref);
+  }
+  if ( exists $hashref->{'svcnum'} ) {
+    #@{ $self->{'_pkgnum'} } = ();
+    my $subcache = $cache->subcache('svcnum', 'cust_svc', $hashref->{pkgnum});
+    $self->{'_svcnum'} = $subcache;
+    #push @{ $self->{'_pkgnum'} },
+    FS::cust_svc->new_or_cached($hashref, $subcache) if $hashref->{svcnum};
+  }
+}
+
+=head1 NAME
+
+FS::cust_pkg - Object methods for cust_pkg objects
+
+=head1 SYNOPSIS
+
+  use FS::cust_pkg;
+
+  $record = new FS::cust_pkg \%hash;
+  $record = new FS::cust_pkg { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->cancel;
+
+  $error = $record->suspend;
+
+  $error = $record->unsuspend;
+
+  $part_pkg = $record->part_pkg;
+
+  @labels = $record->labels;
+
+  $seconds = $record->seconds_since($timestamp);
+
+  $error = FS::cust_pkg::order( $custnum, \@pkgparts );
+  $error = FS::cust_pkg::order( $custnum, \@pkgparts, \@remove_pkgnums ] );
+
+=head1 DESCRIPTION
+
+An FS::cust_pkg object represents a customer billing item.  FS::cust_pkg
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item pkgnum - primary key (assigned automatically for new billing items)
+
+=item custnum - Customer (see L<FS::cust_main>)
+
+=item pkgpart - Billing item definition (see L<FS::part_pkg>)
+
+=item setup - date
+
+=item bill - date (next bill date)
+
+=item last_bill - last bill date
+
+=item susp - date
+
+=item expire - date
+
+=item cancel - date
+
+=item otaker - order taker (assigned automatically if null, see L<FS::UID>)
+
+=item manual_flag - If this field is set to 1, disables the automatic
+unsuspension of this package when using the B<unsuspendauto> config file.
+
+=back
+
+Note: setup, bill, susp, expire and cancel are specified as UNIX timestamps;
+see L<perlfunc/"time">.  Also see L<Time::Local> and L<Date::Parse> for
+conversion functions.
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new billing item.  To add the item to the database, see L<"insert">.
+
+=cut
+
+sub table { 'cust_pkg'; }
+
+=item insert
+
+Adds this billing item to the database ("Orders" the item).  If there is an
+error, returns the error, otherwise returns false.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  # custnum might not have have been defined in sub check (for one-shot new
+  # customers), so check it here instead
+  # (is this still necessary with transactions?)
+
+  my $error = $self->ut_number('custnum');
+  return $error if $error;
+
+  my $cust_main = $self->cust_main;
+  return "Unknown customer ". $self->custnum unless $cust_main;
+
+  unless ( $disable_agentcheck ) {
+    my $agent = qsearchs( 'agent', { 'agentnum' => $cust_main->agentnum } );
+    my $pkgpart_href = $agent->pkgpart_hashref;
+    return "agent ". $agent->agentnum.
+           " can't purchase pkgpart ". $self->pkgpart
+      unless $pkgpart_href->{ $self->pkgpart };
+  }
+
+  $self->SUPER::insert;
+
+}
+
+=item delete
+
+This method now works but you probably shouldn't use it.
+
+You don't want to delete billing items, because there would then be no record
+the customer ever purchased the item.  Instead, see the cancel method.
+
+=cut
+
+#sub delete {
+#  return "Can't delete cust_pkg records!";
+#}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+Currently, custnum, setup, bill, susp, expire, and cancel may be changed.
+
+Changing pkgpart may have disasterous effects.  See the order subroutine.
+
+setup and bill are normally updated by calling the bill method of a customer
+object (see L<FS::cust_main>).
+
+suspend is normally updated by the suspend and unsuspend methods.
+
+cancel is normally updated by the cancel method (and also the order subroutine
+in some cases).
+
+=cut
+
+sub replace {
+  my( $new, $old ) = ( shift, shift );
+
+  #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
+  return "Can't change otaker!" if $old->otaker ne $new->otaker;
+
+  #allow this *sigh*
+  #return "Can't change setup once it exists!"
+  #  if $old->getfield('setup') &&
+  #     $old->getfield('setup') != $new->getfield('setup');
+
+  #some logic for bill, susp, cancel?
+
+  $new->SUPER::replace($old);
+}
+
+=item check
+
+Checks all fields to make sure this is a valid billing item.  If there is an
+error, returns the error, otherwise returns false.  Called by the insert and
+replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('pkgnum')
+    || $self->ut_numbern('custnum')
+    || $self->ut_number('pkgpart')
+    || $self->ut_numbern('setup')
+    || $self->ut_numbern('bill')
+    || $self->ut_numbern('susp')
+    || $self->ut_numbern('cancel')
+  ;
+  return $error if $error;
+
+  if ( $self->custnum ) { 
+    return "Unknown customer ". $self->custnum unless $self->cust_main;
+  }
+
+  return "Unknown pkgpart: ". $self->pkgpart
+    unless qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
+
+  $self->otaker(getotaker) unless $self->otaker;
+  $self->otaker =~ /^([\w\.\-]{0,16})$/ or return "Illegal otaker";
+  $self->otaker($1);
+
+  if ( $self->dbdef_table->column('manual_flag') ) {
+    $self->manual_flag =~ /^([01]?)$/ or return "Illegal manual_flag";
+    $self->manual_flag($1);
+  }
+
+  ''; #no error
+}
+
+=item cancel
+
+Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
+in this package, then cancels the package itself (sets the cancel field to
+now).
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub cancel {
+  my $self = shift;
+  my $error;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE'; 
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_svc (
+    qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
+  ) {
+    my $error = $cust_svc->cancel;
+
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error cancelling cust_svc: $error";
+    }
+
+  }
+
+  unless ( $self->getfield('cancel') ) {
+    my %hash = $self->hash;
+    $hash{'cancel'} = time;
+    my $new = new FS::cust_pkg ( \%hash );
+    $error = $new->replace($self);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  my $conf = new FS::Conf;
+  my @invoicing_list = grep { $_ ne 'POST' } $self->cust_main->invoicing_list;
+  if ( !$quiet && $conf->exists('emailcancel') && @invoicing_list ) {
+    my $conf = new FS::Conf;
+    my $error = send_email(
+      'from'    => $conf->config('invoice_from'),
+      'to'      => \@invoicing_list,
+      'subject' => $conf->config('cancelsubject'),
+      'body'    => [ map "$_\n", $conf->config('cancelmessage') ],
+    );
+    #should this do something on errors?
+  }
+
+  ''; #no errors
+
+}
+
+=item suspend
+
+Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
+package, then suspends the package itself (sets the susp field to now).
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub suspend {
+  my $self = shift;
+  my $error ;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE'; 
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_svc (
+    qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
+  ) {
+    my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
+
+    $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
+      $dbh->rollback if $oldAutoCommit;
+      return "Illegal svcdb value in part_svc!";
+    };
+    my $svcdb = $1;
+    require "FS/$svcdb.pm";
+
+    my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
+    if ($svc) {
+      $error = $svc->suspend;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
+
+  unless ( $self->getfield('susp') ) {
+    my %hash = $self->hash;
+    $hash{'susp'} = time;
+    my $new = new FS::cust_pkg ( \%hash );
+    $error = $new->replace($self);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  ''; #no errors
+}
+
+=item unsuspend
+
+Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
+package, then unsuspends the package itself (clears the susp field).
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub unsuspend {
+  my $self = shift;
+  my($error);
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE'; 
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_svc (
+    qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
+  ) {
+    my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
+
+    $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
+      $dbh->rollback if $oldAutoCommit;
+      return "Illegal svcdb value in part_svc!";
+    };
+    my $svcdb = $1;
+    require "FS/$svcdb.pm";
+
+    my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
+    if ($svc) {
+      $error = $svc->unsuspend;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
+
+  unless ( ! $self->getfield('susp') ) {
+    my %hash = $self->hash;
+    $hash{'susp'} = '';
+    my $new = new FS::cust_pkg ( \%hash );
+    $error = $new->replace($self);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  ''; #no errors
+}
+
+=item last_bill
+
+Returns the last bill date, or if there is no last bill date, the setup date.
+Useful for billing metered services.
+
+=cut
+
+sub last_bill {
+  my $self = shift;
+  if ( $self->dbdef_table->column('last_bill') ) {
+    return $self->setfield('last_bill', $_[0]) if @_;
+    return $self->getfield('last_bill') if $self->getfield('last_bill');
+  }    
+  my $cust_bill_pkg = qsearchs('cust_bill_pkg', { 'pkgnum' => $self->pkgnum,
+                                                  'edate'  => $self->bill,  } );
+  $cust_bill_pkg ? $cust_bill_pkg->sdate : $self->setup || 0;
+}
+
+=item part_pkg
+
+Returns the definition for this billing item, as an FS::part_pkg object (see
+L<FS::part_pkg>).
+
+=cut
+
+sub part_pkg {
+  my $self = shift;
+  #exists( $self->{'_pkgpart'} )
+  $self->{'_pkgpart'}
+    ? $self->{'_pkgpart'}
+    : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
+}
+
+=item cust_svc
+
+Returns the services for this package, as FS::cust_svc objects (see
+L<FS::cust_svc>)
+
+=cut
+
+sub cust_svc {
+  my $self = shift;
+  if ( $self->{'_svcnum'} ) {
+    values %{ $self->{'_svcnum'}->cache };
+  } else {
+    qsearch ( 'cust_svc', { 'pkgnum' => $self->pkgnum } );
+  }
+}
+
+=item labels
+
+Returns a list of lists, calling the label method for all services
+(see L<FS::cust_svc>) of this billing item.
+
+=cut
+
+sub labels {
+  my $self = shift;
+  map { [ $_->label ] } $self->cust_svc;
+}
+
+=item cust_main
+
+Returns the parent customer object (see L<FS::cust_main>).
+
+=cut
+
+sub cust_main {
+  my $self = shift;
+  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+}
+
+=item seconds_since TIMESTAMP
+
+Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
+package have been online since TIMESTAMP, according to the session monitor.
+
+TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub seconds_since {
+  my($self, $since) = @_;
+  my $seconds = 0;
+
+  foreach my $cust_svc (
+    grep { $_->part_svc->svcdb eq 'svc_acct' } $self->cust_svc
+  ) {
+    $seconds += $cust_svc->seconds_since($since);
+  }
+
+  $seconds;
+
+}
+
+=item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
+
+Returns the numbers of seconds all accounts (see L<FS::svc_acct>) in this
+package have been online between TIMESTAMP_START (inclusive) and TIMESTAMP_END
+(exclusive).
+
+TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
+L<perlfunc/"time">.  Also see L<Time::Local> and L<Date::Parse> for conversion
+functions.
+
+
+=cut
+
+sub seconds_since_sqlradacct {
+  my($self, $start, $end) = @_;
+
+  my $seconds = 0;
+
+  foreach my $cust_svc (
+    grep {
+      my $part_svc = $_->part_svc;
+      $part_svc->svcdb eq 'svc_acct'
+        && scalar($part_svc->part_export('sqlradius'));
+    } $self->cust_svc
+  ) {
+    $seconds += $cust_svc->seconds_since_sqlradacct($start, $end);
+  }
+
+  $seconds;
+
+}
+
+=item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
+
+Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
+in this package for sessions ending between TIMESTAMP_START (inclusive) and
+TIMESTAMP_END
+(exclusive).
+
+TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
+L<perlfunc/"time">.  Also see L<Time::Local> and L<Date::Parse> for conversion
+functions.
+
+=cut
+
+sub attribute_since_sqlradacct {
+  my($self, $start, $end, $attrib) = @_;
+
+  my $sum = 0;
+
+  foreach my $cust_svc (
+    grep {
+      my $part_svc = $_->part_svc;
+      $part_svc->svcdb eq 'svc_acct'
+        && scalar($part_svc->part_export('sqlradius'));
+    } $self->cust_svc
+  ) {
+    $sum += $cust_svc->attribute_since_sqlradacct($start, $end, $attrib);
+  }
+
+  $sum;
+
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ] ]
+
+CUSTNUM is a customer (see L<FS::cust_main>)
+
+PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
+L<FS::part_pkg>) to order for this customer.  Duplicates are of course
+permitted.
+
+REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
+remove for this customer.  The services (see L<FS::cust_svc>) are moved to the
+new billing items.  An error is returned if this is not possible (see
+L<FS::pkg_svc>).  An empty arrayref is equivalent to not specifying this
+parameter.
+
+RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
+newly-created cust_pkg objects.
+
+=cut
+
+sub order {
+  my($custnum, $pkgparts, $remove_pkgnums, $return_cust_pkg) = @_;
+  $remove_pkgnums = [] unless defined($remove_pkgnums);
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  # generate %part_pkg
+  # $part_pkg{$pkgpart} is true iff $custnum may purchase $pkgpart
+  #
+  my($cust_main)=qsearchs('cust_main',{'custnum'=>$custnum});
+  my($agent)=qsearchs('agent',{'agentnum'=> $cust_main->agentnum });
+  my %part_pkg = %{ $agent->pkgpart_hashref };
+
+  my(%svcnum);
+  # generate %svcnum
+  # for those packages being removed:
+  #@{ $svcnum{$svcpart} } goes from a svcpart to a list of FS::cust_svc objects
+  my($pkgnum);
+  foreach $pkgnum ( @{$remove_pkgnums} ) {
+    foreach my $cust_svc (qsearch('cust_svc',{'pkgnum'=>$pkgnum})) {
+      push @{ $svcnum{$cust_svc->getfield('svcpart')} }, $cust_svc;
+    }
+  }
+  
+  my @cust_svc;
+  #generate @cust_svc
+  # for those packages the customer is purchasing:
+  # @{$pkgparts} is a list of said packages, by pkgpart
+  # @cust_svc is a corresponding list of lists of FS::Record objects
+  foreach my $pkgpart ( @{$pkgparts} ) {
+    unless ( $part_pkg{$pkgpart} ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Customer not permitted to purchase pkgpart $pkgpart!";
+    }
+    push @cust_svc, [
+      map {
+        ( $svcnum{$_} && @{ $svcnum{$_} } ) ? shift @{ $svcnum{$_} } : ();
+      } map { $_->svcpart }
+          qsearch('pkg_svc', { pkgpart  => $pkgpart,
+                               quantity => { op=>'>', value=>'0', } } )
+    ];
+  }
+
+  #special-case until this can be handled better
+  # move services to new svcparts - even if the svcparts don't match (svcdb
+  # needs to...)
+  # looks like they're moved in no particular order, ewwwwwwww
+  # and looks like just one of each svcpart can be moved... o well
+
+  #start with still-leftover services
+  #foreach my $svcpart ( grep { scalar(@{ $svcnum{$_} }) } keys %svcnum ) {
+  foreach my $svcpart ( keys %svcnum ) {
+    next unless @{ $svcnum{$svcpart} };
+
+    my $svcdb = $svcnum{$svcpart}->[0]->part_svc->svcdb;
+
+    #find an empty place to put one
+    my $i = 0;
+    foreach my $pkgpart ( @{$pkgparts} ) {
+      my @pkg_svc =
+        qsearch('pkg_svc', { pkgpart  => $pkgpart,
+                             quantity => { op=>'>', value=>'0', } } );
+      #my @pkg_svc =
+      #  grep { $_->quantity > 0 } qsearch('pkg_svc', { pkgpart=>$pkgpart } );
+      if ( ! @{$cust_svc[$i]} #find an empty place to put them with 
+           && grep { $svcdb eq $_->part_svc->svcdb } #with appropriate svcdb
+                @pkg_svc
+      ) {
+        my $new_svcpart =
+          ( grep { $svcdb eq $_->part_svc->svcdb } @pkg_svc )[0]->svcpart; 
+        my $cust_svc = shift @{$svcnum{$svcpart}};
+        $cust_svc->svcpart($new_svcpart);
+        #warn "changing from $svcpart to $new_svcpart!!!\n";
+        $cust_svc[$i] = [ $cust_svc ];
+      }
+      $i++;
+    }
+
+  }
+  
+  #check for leftover services
+  foreach (keys %svcnum) {
+    next unless @{ $svcnum{$_} };
+    $dbh->rollback if $oldAutoCommit;
+    return "Leftover services, svcpart $_: svcnum ".
+           join(', ', map { $_->svcnum } @{ $svcnum{$_} } );
+  }
+
+  #no leftover services, let's make changes.
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE'; 
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE'; 
+  local $SIG{PIPE} = 'IGNORE'; 
+
+  #first cancel old packages
+  foreach my $pkgnum ( @{$remove_pkgnums} ) {
+    my($old) = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+    unless ( $old ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Package $pkgnum not found to remove!";
+    }
+    my(%hash) = $old->hash;
+    $hash{'cancel'}=time;   
+    my($new) = new FS::cust_pkg ( \%hash );
+    my($error)=$new->replace($old);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Couldn't update package $pkgnum: $error";
+    }
+  }
+
+  #now add new packages, changing cust_svc records if necessary
+  my $pkgpart;
+  while ($pkgpart=shift @{$pkgparts} ) {
+    my $new = new FS::cust_pkg {
+                                 'custnum' => $custnum,
+                                 'pkgpart' => $pkgpart,
+                               };
+    my $error = $new->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Couldn't insert new cust_pkg record: $error";
+    }
+    push @{$return_cust_pkg}, $new if $return_cust_pkg;
+    my $pkgnum = $new->pkgnum;
+    foreach my $cust_svc ( @{ shift @cust_svc } ) {
+      my(%hash) = $cust_svc->hash;
+      $hash{'pkgnum'}=$pkgnum;
+      my $new = new FS::cust_svc ( \%hash );
+
+      #avoid Record diffing missing changed svcpart field from above.
+      my $old = qsearchs('cust_svc', { 'svcnum' => $cust_svc->svcnum } );
+
+      my $error = $new->replace($old);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "Couldn't link old service to new package: $error";
+      }
+    }
+  }  
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  ''; #no errors
+}
+
+=back
+
+=head1 BUGS
+
+sub order is not OO.  Perhaps it should be moved to FS::cust_main and made so?
+
+In sub order, the @pkgparts array (passed by reference) is clobbered.
+
+Also in sub order, no money is adjusted.  Once FS::part_pkg defines a standard
+method to pass dates to the recur_prog expression, it should do so.
+
+FS::svc_acct, FS::svc_domain, FS::svc_www, FS::svc_ip and FS::svc_forward are
+loaded via 'use' at compile time, rather than via 'require' in sub { setup,
+suspend, unsuspend, cancel } because they use %FS::UID::callback to load
+configuration values.  Probably need a subroutine which decides what to do
+based on whether or not we've fetched the user yet, rather than a hash.  See
+FS::UID and the TODO.
+
+Now that things are transactional should the check in the insert method be
+moved to check ?
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>,
+L<FS::pkg_svc>, schema.html from the base documentation
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_refund.pm b/FS/FS/cust_refund.pm
new file mode 100644 (file)
index 0000000..7636717
--- /dev/null
@@ -0,0 +1,283 @@
+package FS::cust_refund;
+
+use strict;
+use vars qw( @ISA );
+use Business::CreditCard;
+use FS::Record qw( qsearchs dbh );
+use FS::UID qw(getotaker);
+use FS::cust_credit;
+use FS::cust_credit_refund;
+use FS::cust_main;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::cust_refund - Object method for cust_refund objects
+
+=head1 SYNOPSIS
+
+  use FS::cust_refund;
+
+  $record = new FS::cust_refund \%hash;
+  $record = new FS::cust_refund { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_refund represents a refund: the transfer of money to a customer;
+equivalent to a negative payment (see L<FS::cust_pay>).  FS::cust_refund
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item refundnum - primary key (assigned automatically for new refunds)
+
+=item custnum - customer (see L<FS::cust_main>)
+
+=item refund - Amount of the refund
+
+=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=item payby - `CARD' (credit cards), `CHEK' (electronic check/ACH),
+`LECB' (Phone bill billing), `BILL' (billing), or `COMP' (free)
+
+=item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
+
+=item paybatch - text field for tracking card processing
+
+=item otaker - order taker (assigned automatically, see L<FS::UID>)
+
+=item closed - books closed flag, empty or `Y'
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new refund.  To add the refund to the database, see L<"insert">.
+
+=cut
+
+sub table { 'cust_refund'; }
+
+=item insert
+
+Adds this refund to the database.
+
+For backwards-compatibility and convenience, if the additional field crednum is
+defined, an FS::cust_credit_refund record for the full amount of the refund
+will be created.  In this case, custnum is optional.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  if ( $self->crednum ) {
+    my $cust_credit = qsearchs('cust_credit', { 'crednum' => $self->crednum } )
+      or do {
+        $dbh->rollback if $oldAutoCommit;
+        return "Unknown cust_credit.crednum: ". $self->crednum;
+      };
+    $self->custnum($cust_credit->custnum);
+  }
+
+  my $error = $self->check;
+  return $error if $error;
+
+  $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $self->crednum ) {
+    my $cust_credit_refund = new FS::cust_credit_refund {
+      'crednum'   => $self->crednum,
+      'refundnum' => $self->refundnum,
+      'amount'    => $self->refund,
+      '_date'     => $self->_date,
+    };
+    $error = $cust_credit_refund->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+    #$self->custnum($cust_credit_refund->cust_credit->custnum);
+  }
+
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+sub upgrade_replace { #1.3.x->1.4.x
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->check;
+  return $error if $error;
+
+  my %new = $self->hash;
+  my $new = FS::cust_refund->new(\%new);
+
+  if ( $self->crednum ) {
+    my $cust_credit_refund = new FS::cust_credit_refund {
+      'crednum'   => $self->crednum,
+      'refundnum' => $self->refundnum,
+      'amount'    => $self->refund,
+      '_date'     => $self->_date,
+    };
+    $error = $cust_credit_refund->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+    $new->custnum($cust_credit_refund->cust_credit->custnum);
+  } else {
+    die;
+  }
+
+  $error = $new->SUPER::replace($self);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+=item delete
+
+Currently unimplemented (accounting reasons).
+
+=cut
+
+sub delete {
+  my $self = shift;
+  return "Can't delete closed refund" if $self->closed =~ /^Y/i;
+  $self->SUPER::delete(@_);
+}
+
+=item replace OLD_RECORD
+
+Currently unimplemented (accounting reasons).
+
+=cut
+
+sub replace {
+   return "Can't (yet?) modify cust_refund records!";
+}
+
+=item check
+
+Checks all fields to make sure this is a valid refund.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert method.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('refundnum')
+    || $self->ut_numbern('custnum')
+    || $self->ut_money('refund')
+    || $self->ut_numbern('_date')
+    || $self->ut_textn('paybatch')
+    || $self->ut_enum('closed', [ '', 'Y' ])
+  ;
+  return $error if $error;
+
+  return "refund must be > 0 " if $self->refund <= 0;
+
+  $self->_date(time) unless $self->_date;
+
+  return "unknown cust_main.custnum: ". $self->custnum
+    unless $self->crednum 
+           || qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+
+  $self->payby =~ /^(CARD|CHEK|LECB|BILL|COMP)$/ or return "Illegal payby";
+  $self->payby($1);
+
+  #false laziness with cust_pay::check
+  if ( $self->payby eq 'CARD' ) {
+    my $payinfo = $self->payinfo;
+    $payinfo =~ s/\D//g;
+    $self->payinfo($payinfo);
+    if ( $self->payinfo ) {
+      $self->payinfo =~ /^(\d{13,16})$/
+        or return "Illegal (mistyped?) credit card number (payinfo)";
+      $self->payinfo($1);
+      validate($self->payinfo) or return "Illegal credit card number";
+      return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
+    } else {
+      $self->payinfo('N/A');
+    }
+
+  } else {
+    $error = $self->ut_textn('payinfo');
+    return $error if $error;
+  }
+
+  $self->otaker(getotaker);
+
+  ''; #no error
+}
+
+=back
+
+=head1 VERSION
+
+$Id: cust_refund.pm,v 1.20 2002-11-19 09:51:58 ivan Exp $
+
+=head1 BUGS
+
+Delete and replace methods.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_credit>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
new file mode 100644 (file)
index 0000000..c0cb6f4
--- /dev/null
@@ -0,0 +1,525 @@
+package FS::cust_svc;
+
+use strict;
+use vars qw( @ISA $ignore_quantity );
+use Carp qw( cluck );
+use FS::Record qw( qsearch qsearchs dbh );
+use FS::cust_pkg;
+use FS::part_pkg;
+use FS::part_svc;
+use FS::pkg_svc;
+use FS::svc_acct;
+use FS::svc_domain;
+use FS::svc_forward;
+use FS::svc_broadband;
+use FS::domain_record;
+use FS::part_export;
+
+@ISA = qw( FS::Record );
+
+$ignore_quantity = 0;
+
+sub _cache {
+  my $self = shift;
+  my ( $hashref, $cache ) = @_;
+  if ( $hashref->{'username'} ) {
+    $self->{'_svc_acct'} = FS::svc_acct->new($hashref, '');
+  }
+  if ( $hashref->{'svc'} ) {
+    $self->{'_svcpart'} = FS::part_svc->new($hashref);
+  }
+}
+
+=head1 NAME
+
+FS::cust_svc - Object method for cust_svc objects
+
+=head1 SYNOPSIS
+
+  use FS::cust_svc;
+
+  $record = new FS::cust_svc \%hash
+  $record = new FS::cust_svc { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  ($label, $value) = $record->label;
+
+=head1 DESCRIPTION
+
+An FS::cust_svc represents a service.  FS::cust_svc inherits from FS::Record.
+The following fields are currently supported:
+
+=over 4
+
+=item svcnum - primary key (assigned automatically for new services)
+
+=item pkgnum - Package (see L<FS::cust_pkg>)
+
+=item svcpart - Service definition (see L<FS::part_svc>)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new service.  To add the refund to the database, see L<"insert">.
+Services are normally created by creating FS::svc_ objects (see
+L<FS::svc_acct>, L<FS::svc_domain>, and L<FS::svc_forward>, among others).
+
+=cut
+
+sub table { 'cust_svc'; }
+
+=item insert
+
+Adds this service to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this service from the database.  If there is an error, returns the
+error, otherwise returns false.  Note that this only removes the cust_svc
+record - you should probably use the B<cancel> method instead.
+
+=item cancel
+
+Cancels the relevant service by calling the B<cancel> method of the associated
+FS::svc_XXX object (i.e. an FS::svc_acct object or FS::svc_domain object),
+deleting the FS::svc_XXX record and then deleting this record.
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub cancel {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE'; 
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $part_svc = $self->part_svc;
+
+  $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
+    $dbh->rollback if $oldAutoCommit;
+    return "Illegal svcdb value in part_svc!";
+  };
+  my $svcdb = $1;
+  require "FS/$svcdb.pm";
+
+  my $svc = $self->svc_x;
+  if ($svc) {
+    my $error = $svc->cancel;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error canceling service: $error";
+    }
+    $error = $svc->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error deleting service: $error";
+    }
+  }
+
+  my $error = $self->delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Error deleting cust_svc: $error";
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  ''; #no errors
+
+}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $new->SUPER::replace($old);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error if $error;
+  }
+
+  if ( $new->svcpart != $old->svcpart ) {
+    my $svc_x = $new->svc_x;
+    my $new_svc_x = ref($svc_x)->new({$svc_x->hash});
+    my $error = $new_svc_x->replace($svc_x);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error if $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+
+}
+
+=item check
+
+Checks all fields to make sure this is a valid service.  If there is an error,
+returns the error, otehrwise returns false.  Called by the insert and
+replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('svcnum')
+    || $self->ut_numbern('pkgnum')
+    || $self->ut_number('svcpart')
+  ;
+  return $error if $error;
+
+  my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
+  return "Unknown svcpart" unless $part_svc;
+
+  if ( $self->pkgnum ) {
+    my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
+    return "Unknown pkgnum" unless $cust_pkg;
+    my $pkg_svc = qsearchs( 'pkg_svc', {
+      'pkgpart' => $cust_pkg->pkgpart,
+      'svcpart' => $self->svcpart,
+    });
+    # or new FS::pkg_svc ( { 'pkgpart'  => $cust_pkg->pkgpart,
+    #                        'svcpart'  => $self->svcpart,
+    #                        'quantity' => 0                   } );
+    my $quantity = $pkg_svc ? $pkg_svc->quantity : 0;
+
+    my @cust_svc = qsearch('cust_svc', {
+      'pkgnum'  => $self->pkgnum,
+      'svcpart' => $self->svcpart,
+    });
+    return "Already ". scalar(@cust_svc). " ". $part_svc->svc.
+           " services for pkgnum ". $self->pkgnum
+      if scalar(@cust_svc) >= $quantity && (!$ignore_quantity || !$quantity);
+  }
+
+  ''; #no error
+}
+
+=item part_svc
+
+Returns the definition for this service, as a FS::part_svc object (see
+L<FS::part_svc>).
+
+=cut
+
+sub part_svc {
+  my $self = shift;
+  $self->{'_svcpart'}
+    ? $self->{'_svcpart'}
+    : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
+}
+
+=item cust_pkg
+
+Returns the definition for this service, as a FS::part_svc object (see
+L<FS::part_svc>).
+
+=cut
+
+sub cust_pkg {
+  my $self = shift;
+  qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
+}
+
+=item label
+
+Returns a list consisting of:
+- The name of this service (from part_svc)
+- A meaningful identifier (username, domain, or mail alias)
+- The table name (i.e. svc_domain) for this service
+
+=cut
+
+sub label {
+  my $self = shift;
+  my $svcdb = $self->part_svc->svcdb;
+  my $svc_x = $self->svc_x
+    or die "can't find $svcdb.svcnum ". $self->svcnum;
+  my $tag;
+  if ( $svcdb eq 'svc_acct' ) {
+    $tag = $svc_x->email;
+  } elsif ( $svcdb eq 'svc_forward' ) {
+    my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_x->srcsvc } );
+    $tag = $svc_acct->email. '->';
+    if ( $svc_x->dstsvc ) {
+      $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_x->dstsvc } );
+      $tag .= $svc_acct->email;
+    } else {
+      $tag .= $svc_x->dst;
+    }
+  } elsif ( $svcdb eq 'svc_domain' ) {
+    $tag = $svc_x->getfield('domain');
+  } elsif ( $svcdb eq 'svc_www' ) {
+    my $domain = qsearchs( 'domain_record', { 'recnum' => $svc_x->recnum } );
+    $tag = $domain->zone;
+  } elsif ( $svcdb eq 'svc_broadband' ) {
+    $tag = $svc_x->ip_addr;
+  } else {
+    cluck "warning: asked for label of unsupported svcdb; using svcnum";
+    $tag = $svc_x->getfield('svcnum');
+  }
+  $self->part_svc->svc, $tag, $svcdb;
+}
+
+=item svc_x
+
+Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
+FS::svc_domain object, etc.)
+
+=cut
+
+sub svc_x {
+  my $self = shift;
+  my $svcdb = $self->part_svc->svcdb;
+  if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
+    $self->{'_svc_acct'};
+  } else {
+    qsearchs( $svcdb, { 'svcnum' => $self->svcnum } );
+  }
+}
+
+=item seconds_since TIMESTAMP
+
+See L<FS::svc_acct/seconds_since>.  Equivalent to
+$cust_svc->svc_x->seconds_since, but more efficient.  Meaningless for records
+where B<svcdb> is not "svc_acct".
+
+=cut
+
+#note: implementation here, POD in FS::svc_acct
+sub seconds_since {
+  my($self, $since) = @_;
+  my $dbh = dbh;
+  my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
+                              WHERE svcnum = ?
+                                AND login >= ?
+                                AND logout IS NOT NULL'
+  ) or die $dbh->errstr;
+  $sth->execute($self->svcnum, $since) or die $sth->errstr;
+  $sth->fetchrow_arrayref->[0];
+}
+
+=item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
+
+See L<FS::svc_acct/seconds_since_sqlradacct>.  Equivalent to
+$cust_svc->svc_x->seconds_since_sqlradacct, but more efficient.  Meaningless
+for records where B<svcdb> is not "svc_acct".
+
+=cut
+
+#note: implementation here, POD in FS::svc_acct
+sub seconds_since_sqlradacct {
+  my($self, $start, $end) = @_;
+
+  my $username = $self->svc_x->username;
+
+  my @part_export = $self->part_svc->part_export('sqlradius')
+    or die "no sqlradius export configured for this service type";
+    #or return undef;
+
+  my $seconds = 0;
+  foreach my $part_export ( @part_export ) {
+
+    my $dbh = DBI->connect( map { $part_export->option($_) }
+                            qw(datasrc username password)    )
+      or die "can't connect to sqlradius database: ". $DBI::errstr;
+
+    #select a unix time conversion function based on database type
+    my $str2time;
+    if ( $dbh->{Driver}->{Name} eq 'mysql' ) {
+      $str2time = 'UNIX_TIMESTAMP(';
+    } elsif ( $dbh->{Driver}->{Name} eq 'Pg' ) {
+      $str2time = 'EXTRACT( EPOCH FROM ';
+    } else {
+      warn "warning: unknown database type ". $dbh->{Driver}->{Name}.
+           "; guessing how to convert to UNIX timestamps";
+      $str2time = 'extract(epoch from ';
+    }
+
+    my $query;
+  
+    #find closed sessions completely within the given range
+    my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
+                               FROM radacct
+                               WHERE UserName = ?
+                                 AND $str2time AcctStartTime) >= ?
+                                 AND $str2time AcctStopTime ) <  ?
+                                 AND $str2time AcctStopTime ) > 0
+                                 AND AcctStopTime IS NOT NULL"
+    ) or die $dbh->errstr;
+    $sth->execute($username, $start, $end) or die $sth->errstr;
+    my $regular = $sth->fetchrow_arrayref->[0];
+  
+    #find open sessions which start in the range, count session start->range end
+    $query = "SELECT SUM( ? - $str2time AcctStartTime ) )
+                FROM radacct
+                WHERE UserName = ?
+                  AND $str2time AcctStartTime ) >= ?
+                  AND $str2time AcctStartTime ) <  ?
+                  AND ( ? - $str2time AcctStartTime ) ) < 86400
+                  AND (    $str2time AcctStopTime ) = 0
+                                    OR AcctStopTime IS NULL )";
+    $sth = $dbh->prepare($query) or die $dbh->errstr;
+    $sth->execute($end, $username, $start, $end, $end)
+      or die $sth->errstr. " executing query $query";
+    my $start_during = $sth->fetchrow_arrayref->[0];
+  
+    #find closed sessions which start before the range but stop during,
+    #count range start->session end
+    $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? ) 
+                            FROM radacct
+                            WHERE UserName = ?
+                              AND $str2time AcctStartTime ) < ?
+                              AND $str2time AcctStopTime  ) >= ?
+                              AND $str2time AcctStopTime  ) <  ?
+                              AND $str2time AcctStopTime ) > 0
+                              AND AcctStopTime IS NOT NULL"
+    ) or die $dbh->errstr;
+    $sth->execute($start, $username, $start, $start, $end ) or die $sth->errstr;
+    my $end_during = $sth->fetchrow_arrayref->[0];
+  
+    #find closed (not anymore - or open) sessions which start before the range
+    # but stop after, or are still open, count range start->range end
+    # don't count open sessions (probably missing stop record)
+    $sth = $dbh->prepare("SELECT COUNT(*)
+                            FROM radacct
+                            WHERE UserName = ?
+                              AND $str2time AcctStartTime ) < ?
+                              AND ( $str2time AcctStopTime ) >= ?
+                                                                  )"
+                              #      OR AcctStopTime =  0
+                              #      OR AcctStopTime IS NULL       )"
+    ) or die $dbh->errstr;
+    $sth->execute($username, $start, $end ) or die $sth->errstr;
+    my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
+
+    $seconds += $regular + $end_during + $start_during + $entire_range;
+
+  }
+
+  $seconds;
+
+}
+
+=item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
+
+See L<FS::svc_acct/attribute_since_sqlradacct>.  Equivalent to
+$cust_svc->svc_x->attribute_since_sqlradacct, but more efficient.  Meaningless
+for records where B<svcdb> is not "svc_acct".
+
+=cut
+
+#note: implementation here, POD in FS::svc_acct
+#(false laziness w/seconds_since_sqlradacct above)
+sub attribute_since_sqlradacct {
+  my($self, $start, $end, $attrib) = @_;
+
+  my $username = $self->svc_x->username;
+
+  my @part_export = $self->part_svc->part_export('sqlradius')
+    or die "no sqlradius export configured for this service type";
+    #or return undef;
+
+  my $sum = 0;
+
+  foreach my $part_export ( @part_export ) {
+
+    my $dbh = DBI->connect( map { $part_export->option($_) }
+                            qw(datasrc username password)    )
+      or die "can't connect to sqlradius database: ". $DBI::errstr;
+
+    #select a unix time conversion function based on database type
+    my $str2time;
+    if ( $dbh->{Driver}->{Name} eq 'mysql' ) {
+      $str2time = 'UNIX_TIMESTAMP(';
+    } elsif ( $dbh->{Driver}->{Name} eq 'Pg' ) {
+      $str2time = 'EXTRACT( EPOCH FROM ';
+    } else {
+      warn "warning: unknown database type ". $dbh->{Driver}->{Name}.
+           "; guessing how to convert to UNIX timestamps";
+      $str2time = 'extract(epoch from ';
+    }
+
+    my $sth = $dbh->prepare("SELECT SUM($attrib)
+                               FROM radacct
+                               WHERE UserName = ?
+                                 AND $str2time AcctStopTime ) >= ?
+                                 AND $str2time AcctStopTime ) <  ?
+                                 AND AcctStopTime IS NOT NULL"
+    ) or die $dbh->errstr;
+    $sth->execute($username, $start, $end) or die $sth->errstr;
+
+    $sum += $sth->fetchrow_arrayref->[0];
+
+  }
+
+  $sum;
+
+}
+
+=back
+
+=head1 BUGS
+
+Behaviour of changing the svcpart of cust_svc records is undefined and should
+possibly be prohibited, and pkg_svc records are not checked.
+
+pkg_svc records are not checked in general (here).
+
+Deleting this record doesn't check or delete the svc_* record associated
+with this record.
+
+In seconds_since_sqlradacct, specifying a DATASRC/USERNAME/PASSWORD instead of
+a DBI database handle is not yet implemented.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>, 
+schema.html from the base documentation
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_tax_exempt.pm b/FS/FS/cust_tax_exempt.pm
new file mode 100644 (file)
index 0000000..ab873c0
--- /dev/null
@@ -0,0 +1,131 @@
+package FS::cust_tax_exempt;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::cust_tax_exempt - Object methods for cust_tax_exempt records
+
+=head1 SYNOPSIS
+
+  use FS::cust_tax_exempt;
+
+  $record = new FS::cust_tax_exempt \%hash;
+  $record = new FS::cust_tax_exempt { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_tax_exempt object represents a historical record of a customer tax
+exemption.  Currently this is only used for "texas tax".  FS::cust_tax_exempt
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item exemptnum - primary key
+
+=item custnum - customer (see L<FS::cust_main>)
+
+=item taxnum - tax rate (see L<FS::cust_main_county>)
+
+=item year
+
+=item month
+
+=item amount
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new exemption record.  To add the example to the database, see
+L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'cust_tax_exempt'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  $self->ut_numbern('exemptnum')
+    || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
+    || $self->ut_foreign_key('taxnum', 'cust_main_county', 'taxnum')
+    || $self->ut_number('year') #check better
+    || $self->ut_number('month') #check better
+    || $self->ut_money('amount')
+  ;
+}
+
+=back
+
+=head1 BUGS
+
+Texas tax is a royal pain in the ass.
+
+=head1 SEE ALSO
+
+L<FS::cust_main_county>, L<FS::cust_main>, L<FS::Record>, schema.html from the
+base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/domain_record.pm b/FS/FS/domain_record.pm
new file mode 100644 (file)
index 0000000..77b9550
--- /dev/null
@@ -0,0 +1,351 @@
+package FS::domain_record;
+
+use strict;
+use vars qw( @ISA $noserial_hack );
+#use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearchs dbh );
+use FS::svc_domain;
+use FS::svc_www;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::domain_record - Object methods for domain_record records
+
+=head1 SYNOPSIS
+
+  use FS::domain_record;
+
+  $record = new FS::domain_record \%hash;
+  $record = new FS::domain_record { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::domain_record object represents an entry in a DNS zone.
+FS::domain_record inherits from FS::Record.  The following fields are currently
+supported:
+
+=over 4
+
+=item recnum - primary key
+
+=item svcnum - Domain (see L<FS::svc_domain>) of this entry
+
+=item reczone - partial (or full) zone for this entry
+
+=item recaf - address family for this entry, currently only `IN' is recognized.
+
+=item rectype - record type for this entry (A, MX, etc.)
+
+=item recdata - data for this entry
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new entry.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'domain_record'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  if ( $self->rectype eq '_mstr' ) { #delete all other records
+    foreach my $domain_record ( reverse $self->svc_domain->domain_record ) {
+      my $error = $domain_record->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  unless ( $self->rectype =~ /^(SOA|_mstr)$/ ) {
+    my $error = $self->increment_serial;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+  my $self = shift;
+
+  return "Can't delete a domain record which has a website!"
+    if qsearchs( 'svc_www', { 'recnum' => $self->recnum } );
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  unless ( $self->rectype =~ /^(SOA|_mstr)$/ ) {
+    my $error = $self->increment_serial;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::replace(@_);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  unless ( $self->rectype eq 'SOA' ) {
+    my $error = $self->increment_serial;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('recnum')
+    || $self->ut_number('svcnum')
+  ;
+  return $error if $error;
+
+  return "Unknown svcnum (in svc_domain)"
+    unless qsearchs('svc_domain', { 'svcnum' => $self->svcnum } );
+
+  $self->reczone =~ /^(@|[a-z0-9\.\-\*]+)$/i
+    or return "Illegal reczone: ". $self->reczone;
+  $self->reczone($1);
+
+  $self->recaf =~ /^(IN)$/ or return "Illegal recaf: ". $self->recaf;
+  $self->recaf($1);
+
+  $self->rectype =~ /^(SOA|NS|MX|A|PTR|CNAME|_mstr)$/
+    or return "Illegal rectype (only SOA NS MX A PTR CNAME recognized): ".
+              $self->rectype;
+  $self->rectype($1);
+
+  return "Illegal reczone for ". $self->rectype. ": ". $self->reczone
+    if $self->rectype !~ /^MX$/i && $self->reczone =~ /\*/;
+
+  if ( $self->rectype eq 'SOA' ) {
+    my $recdata = $self->recdata;
+    $recdata =~ s/\s+/ /g;
+    $recdata =~ /^([a-z0-9\.\-]+ [\w\-\+]+\.[a-z0-9\.\-]+ \( ((\d+|((\d+[WDHMS])+)) ){5}\))$/i
+      or return "Illegal data for SOA record: $recdata";
+    $self->recdata($1);
+  } elsif ( $self->rectype eq 'NS' ) {
+    $self->recdata =~ /^([a-z0-9\.\-]+)$/i
+      or return "Illegal data for NS record: ". $self->recdata;
+    $self->recdata($1);
+  } elsif ( $self->rectype eq 'MX' ) {
+    $self->recdata =~ /^(\d+)\s+([a-z0-9\.\-]+)$/i
+      or return "Illegal data for MX record: ". $self->recdata;
+    $self->recdata("$1 $2");
+  } elsif ( $self->rectype eq 'A' ) {
+    $self->recdata =~ /^((\d{1,3}\.){3}\d{1,3})$/
+      or return "Illegal data for A record: ". $self->recdata;
+    $self->recdata($1);
+  } elsif ( $self->rectype eq 'PTR' ) {
+    $self->recdata =~ /^([a-z0-9\.\-]+)$/i
+      or return "Illegal data for PTR record: ". $self->recdata;
+    $self->recdata($1);
+  } elsif ( $self->rectype eq 'CNAME' ) {
+    $self->recdata =~ /^([a-z0-9\.\-]+|\@)$/i
+      or return "Illegal data for CNAME record: ". $self->recdata;
+    $self->recdata($1);
+  } elsif ( $self->rectype eq '_mstr' ) {
+    $self->recdata =~ /^((\d{1,3}\.){3}\d{1,3})$/
+      or return "Illegal data for _master pseudo-record: ". $self->recdata;
+  } else {
+    die "ack!";
+  }
+
+  ''; #no error
+}
+
+=item increment_serial
+
+=cut
+
+sub increment_serial {
+  return '' if $noserial_hack;
+  my $self = shift;
+
+  my $soa = qsearchs('domain_record', {
+    svcnum  => $self->svcnum,
+    reczone => '@', #or full domain ?
+    recaf   => 'IN',
+    rectype => 'SOA', 
+  } ) or return "soa record not found; can't increment serial";
+
+  my $data = $soa->recdata;
+  $data =~ s/(\(\D*)(\d+)/$1.($2+1)/e; #well, it works.
+
+  my %hash = $soa->hash;
+  $hash{recdata} = $data;
+  my $new = new FS::domain_record \%hash;
+  $new->replace($soa);
+}
+
+=item svc_domain
+
+Returns the domain (see L<FS::svc_domain>) for this record.
+
+=cut
+
+sub svc_domain {
+  my $self = shift;
+  qsearchs('svc_domain', { svcnum => $self->svcnum } );
+}
+
+=item zone
+
+Returns the canonical zone name.
+
+=cut
+
+sub zone {
+  my $self = shift;
+  my $zone = $self->reczone; # or die ?
+  if ( $zone =~ /\.$/ ) {
+    $zone =~ s/\.$//;
+  } else {
+    my $svc_domain = $self->svc_domain; # or die ?
+    $zone .= '.'. $svc_domain->domain;
+    $zone =~ s/^\@\.//;
+  }
+  $zone;
+}
+
+=back
+
+=head1 VERSION
+
+$Id: domain_record.pm,v 1.15 2003-04-29 18:28:50 khoff Exp $
+
+=head1 BUGS
+
+The data validation doesn't check everything it could.  In particular,
+there is no protection against bad data that passes the regex, duplicate
+SOA records, forgetting the trailing `.', impossible IP addersses, etc.  Of
+course, it's still better than editing the zone files directly.  :)
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/export_svc.pm b/FS/FS/export_svc.pm
new file mode 100644 (file)
index 0000000..da9ac69
--- /dev/null
@@ -0,0 +1,123 @@
+package FS::export_svc;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+use FS::part_export;
+use FS::part_svc;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::export_svc - Object methods for export_svc records
+
+=head1 SYNOPSIS
+
+  use FS::export_svc;
+
+  $record = new FS::export_svc \%hash;
+  $record = new FS::export_svc { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::export_svc object links a service definition (see L<FS::part_svc>) to
+an export (see L<FS::part_export>).  FS::export_svc inherits from FS::Record.
+The following fields are currently supported:
+
+=over 4
+
+=item exportsvcnum - primary key
+
+=item exportnum - export (see L<FS::part_export>)
+
+=item svcpart - service definition (see L<FS::part_svc>)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'export_svc'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  $self->ut_numbern('exportsvcnum')
+    || $self->ut_number('exportnum')
+    || $self->ut_foreign_key('exportnum', 'part_export', 'exportnum')
+    || $self->ut_number('svcpart')
+    || $self->ut_foreign_key('svcpart', 'part_svc', 'svcpart')
+  ;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::part_export>, L<FS::part_svc>, L<FS::Record>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/msgcat.pm b/FS/FS/msgcat.pm
new file mode 100644 (file)
index 0000000..fa10d34
--- /dev/null
@@ -0,0 +1,132 @@
+package FS::msgcat;
+
+use strict;
+use vars qw( @ISA );
+use Exporter;
+use FS::UID;
+use FS::Record qw( qsearchs );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::msgcat - Object methods for message catalog entries
+
+=head1 SYNOPSIS
+
+  use FS::msgcat;
+
+  $record = new FS::msgcat \%hash;
+  $record = new FS::msgcat { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::msgcat object represents an message catalog entry.  FS::msgcat inherits 
+from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item msgnum - primary key
+
+=item msgcode - Error code
+
+=item locale - Locale
+
+=item msg - Message
+
+=back
+
+If you just want to B<use> message catalogs, see L<FS::Msgcat>.
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'msgcat'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('msgnum')
+    || $self->ut_text('msgcode')
+    || $self->ut_text('msg')
+  ;
+  return $error if $error;
+
+  $self->locale =~ /^([\w\@]+)$/ or return "illegal locale: ". $self->locale;
+  $self->locale($1);
+
+  ''; #no error
+}
+
+=back
+
+=head1 BUGS
+
+i18n/l10n, eek
+
+=head1 SEE ALSO
+
+L<FS::Msgcat>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/nas.pm b/FS/FS/nas.pm
new file mode 100644 (file)
index 0000000..58c6827
--- /dev/null
@@ -0,0 +1,152 @@
+package FS::nas;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw(qsearchs); #qsearch);
+use FS::UID qw( dbh );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::nas - Object methods for nas records
+
+=head1 SYNOPSIS
+
+  use FS::nas;
+
+  $record = new FS::nas \%hash;
+  $record = new FS::nas {
+    'nasnum'  => 1,
+    'nasip'   => '10.4.20.23',
+    'nasfqdn' => 'box1.brc.nv.us.example.net',
+  };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->heartbeat($timestamp);
+
+=head1 DESCRIPTION
+
+An FS::nas object represents an Network Access Server on your network, such as
+a terminal server or equivalent.  FS::nas inherits from FS::Record.  The
+following fields are currently supported:
+
+=over 4
+
+=item nasnum - primary key
+
+=item nas - NAS name
+
+=item nasip - NAS ip address
+
+=item nasfqdn - NAS fully-qualified domain name
+
+=item last - timestamp indicating the last instant the NAS was in a known
+             state (used by the session monitoring).
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new NAS.  To add the NAS to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'nas'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  $self->ut_numbern('nasnum')
+    || $self->ut_text('nas')
+    || $self->ut_ip('nasip')
+    || $self->ut_domain('nasfqdn')
+    || $self->ut_numbern('last');
+}
+
+=item heartbeat TIMESTAMP
+
+Updates the timestamp for this nas
+
+=cut
+
+sub heartbeat {
+  my($self, $timestamp) = @_;
+  my $dbh = dbh;
+  my $sth =
+    $dbh->prepare("UPDATE nas SET last = ? WHERE nasnum = ? AND last < ?");
+  $sth->execute($timestamp, $self->nasnum, $timestamp) or die $sth->errstr;
+  $self->last($timestamp);
+}
+
+=back
+
+=head1 VERSION
+
+$Id: nas.pm,v 1.6 2002-03-04 12:48:49 ivan Exp $
+
+=head1 BUGS
+
+heartbeat method uses SQL directly and doesn't update history tables.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_bill_event.pm b/FS/FS/part_bill_event.pm
new file mode 100644 (file)
index 0000000..e0e4f3f
--- /dev/null
@@ -0,0 +1,183 @@
+package FS::part_bill_event;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+use FS::Conf;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::part_bill_event - Object methods for part_bill_event records
+
+=head1 SYNOPSIS
+
+  use FS::part_bill_event;
+
+  $record = new FS::part_bill_event \%hash;
+  $record = new FS::part_bill_event { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_bill_event object represents an invoice event definition -
+a callback which is triggered when an invoice is a certain amount of time
+overdue.  FS::part_bill_event inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item eventpart - primary key
+
+=item payby - CARD, DCRD, CHEK, DCHK, LECB, BILL, or COMP
+
+=item event - event name
+
+=item eventcode - event action
+
+=item seconds - how long after the invoice date events of this type are triggered
+
+=item weight - ordering for events with identical seconds
+
+=item plan - eventcode plan
+
+=item plandata - additional plan data
+
+=item disabled - Disabled flag, empty or `Y'
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new invoice event definition.  To add the example to the database,
+see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_bill_event'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid invoice event definition.  If
+there is an error, returns the error, otherwise returns false.  Called by the
+insert and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  $self->weight(0) unless $self->weight;
+
+  my $conf = new FS::Conf;
+  if ( $conf->exists('safe-part_bill_event') ) {
+    my $error = $self->ut_anything('eventcode');
+    return $error if $error;
+
+    my $c = $self->eventcode;
+
+    $c =~ /^\s*\$cust_main\->(suspend|cancel|invoicing_list_addpost|bill|collect)\(\);\s*("";)?\s*$/
+
+      or $c =~ /^\s*\$cust_bill\->(comp|realtime_(card|ach|lec)|realtime_card_cybercash|batch_card|send)\(\);\s*$/
+
+      or $c =~ /^\s*\$cust_bill\->send\(\'\w+\'\);\s*$/
+
+      or $c =~ /^\s*\$cust_main\->apply_payments; \$cust_main->apply_credits; "";\s*$/
+
+      or $c =~ /^\s*\$cust_main\->charge\( \s*\d*\.?\d*\s*,\s*\'[\w \!\@\#\$\%\&\(\)\-\+\;\:\"\,\.\?\/]*\'\s*\);\s*$/
+
+      or do {
+        #log
+        return "illegal eventcode: $c";
+      };
+
+  }
+
+  my $error = $self->ut_numbern('eventpart')
+    || $self->ut_enum('payby', [qw( CARD DCRD CHEK DCHK LECB BILL COMP )] )
+    || $self->ut_text('event')
+    || $self->ut_anything('eventcode')
+    || $self->ut_number('seconds')
+    || $self->ut_enum('disabled', [ '', 'Y' ] )
+    || $self->ut_number('weight')
+    || $self->ut_textn('plan')
+    || $self->ut_anything('plandata')
+  ;
+  return $error if $error;
+
+  #quelle kludge
+  if ( $self->plandata =~ /^templatename\s+(.*)$/ ) {
+    my $name= $1;
+    unless ( $conf->exists("invoice_template_$name") ) {
+      $conf->set(
+        "invoice_template_$name" =>
+          join("\n", $conf->config('invoice_template') )
+      );
+    }
+  }
+
+  '';
+
+}
+
+=back
+
+=head1 BUGS
+
+Alas.
+
+=head1 SEE ALSO
+
+L<FS::cust_bill>, L<FS::cust_bill_event>, L<FS::Record>, schema.html from the
+base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm
new file mode 100644 (file)
index 0000000..ff51996
--- /dev/null
@@ -0,0 +1,1045 @@
+package FS::part_export;
+
+use strict;
+use vars qw( @ISA @EXPORT_OK %exports );
+use Exporter;
+use Tie::IxHash;
+use FS::Record qw( qsearch qsearchs dbh );
+use FS::part_svc;
+use FS::part_export_option;
+use FS::export_svc;
+
+@ISA = qw(FS::Record);
+@EXPORT_OK = qw(export_info);
+
+=head1 NAME
+
+FS::part_export - Object methods for part_export records
+
+=head1 SYNOPSIS
+
+  use FS::part_export;
+
+  $record = new FS::part_export \%hash;
+  $record = new FS::part_export { 'column' => 'value' };
+
+  #($new_record, $options) = $template_recored->clone( $svcpart );
+
+  $error = $record->insert( { 'option' => 'value' } );
+  $error = $record->insert( \%options );
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_export object represents an export of Freeside data to an external
+provisioning system.  FS::part_export inherits from FS::Record.  The following
+fields are currently supported:
+
+=over 4
+
+=item exportnum - primary key
+
+=item machine - Machine name 
+
+=item exporttype - Export type
+
+=item nodomain - blank or "Y" : usernames are exported to this service with no domain
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new export.  To add the export to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_export'; }
+
+=cut
+
+#=item clone SVCPART
+#
+#An alternate constructor.  Creates a new export by duplicating an existing
+#export.  The given svcpart is assigned to the new export.
+#
+#Returns a list consisting of the new export object and a hashref of options.
+#
+#=cut
+#
+#sub clone {
+#  my $self = shift;
+#  my $class = ref($self);
+#  my %hash = $self->hash;
+#  $hash{'exportnum'} = '';
+#  $hash{'svcpart'} = shift;
+#  ( $class->new( \%hash ),
+#    { map { $_->optionname => $_->optionvalue }
+#        qsearch('part_export_option', { 'exportnum' => $self->exportnum } )
+#    }
+#  );
+#}
+
+=item insert HASHREF
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+If a hash reference of options is supplied, part_export_option records are
+created (see L<FS::part_export_option>).
+
+=cut
+
+#false laziness w/queue.pm
+sub insert {
+  my $self = shift;
+  my $options = shift;
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  foreach my $optionname ( keys %{$options} ) {
+    my $part_export_option = new FS::part_export_option ( {
+      'exportnum'   => $self->exportnum,
+      'optionname'  => $optionname,
+      'optionvalue' => $options->{$optionname},
+    } );
+    $error = $part_export_option->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+#foreign keys would make this much less tedious... grr dumb mysql
+sub delete {
+  my $self = shift;
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  foreach my $part_export_option ( $self->part_export_option ) {
+    my $error = $part_export_option->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  foreach my $export_svc ( $self->export_svc ) {
+    my $error = $export_svc->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+=item replace OLD_RECORD HASHREF
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+If a hash reference of options is supplied, part_export_option records are
+created or modified (see L<FS::part_export_option>).
+
+=cut
+
+sub replace {
+  my $self = shift;
+  my $old = shift;
+  my $options = shift;
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::replace($old);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  foreach my $optionname ( keys %{$options} ) {
+    my $old = qsearchs( 'part_export_option', {
+        'exportnum'   => $self->exportnum,
+        'optionname'  => $optionname,
+    } );
+    my $new = new FS::part_export_option ( {
+        'exportnum'   => $self->exportnum,
+        'optionname'  => $optionname,
+        'optionvalue' => $options->{$optionname},
+    } );
+    $new->optionnum($old->optionnum) if $old;
+    my $error = $old ? $new->replace($old) : $new->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  #remove extraneous old options
+  foreach my $opt (
+    grep { !exists $options->{$_->optionname} } $old->part_export_option
+  ) {
+    my $error = $opt->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+};
+
+=item check
+
+Checks all fields to make sure this is a valid export.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+  my $error = 
+    $self->ut_numbern('exportnum')
+    || $self->ut_domain('machine')
+    || $self->ut_alpha('exporttype')
+  ;
+  return $error if $error;
+
+  $self->machine =~ /^([\w\-\.]*)$/
+    or return "Illegal machine: ". $self->machine;
+  $self->machine($1);
+
+  $self->nodomain =~ /^(Y?)$/ or return "Illegal nodomain: ". $self->nodomain;
+  $self->nodomain($1);
+
+  $self->deprecated(1); #BLAH
+
+  #check exporttype?
+
+  ''; #no error
+}
+
+#=item part_svc
+#
+#Returns the service definition (see L<FS::part_svc>) for this export.
+#
+#=cut
+#
+#sub part_svc {
+#  my $self = shift;
+#  qsearchs('part_svc', { svcpart => $self->svcpart } );
+#}
+
+sub part_svc {
+  use Carp;
+  croak "FS::part_export::part_svc deprecated";
+  #confess "FS::part_export::part_svc deprecated";
+}
+
+=item svc_x
+
+Returns a list of associated FS::svc_* records.
+
+=cut
+
+sub svc_x {
+  my $self = shift;
+  map { $_->svc_x } $self->cust_svc;
+}
+
+=item cust_svc
+
+Returns a list of associated FS::cust_svc records.
+
+=cut
+
+sub cust_svc {
+  my $self = shift;
+  map { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
+    grep { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
+      $self->export_svc;
+}
+
+=item export_svc
+
+Returns a list of associated FS::export_svc records.
+
+=cut
+
+sub export_svc {
+  my $self = shift;
+  qsearch('export_svc', { 'exportnum' => $self->exportnum } );
+}
+
+=item part_export_option
+
+Returns all options as FS::part_export_option objects (see
+L<FS::part_export_option>).
+
+=cut
+
+sub part_export_option {
+  my $self = shift;
+  qsearch('part_export_option', { 'exportnum' => $self->exportnum } );
+}
+
+=item options 
+
+Returns a list of option names and values suitable for assigning to a hash.
+
+=cut
+
+sub options {
+  my $self = shift;
+  map { $_->optionname => $_->optionvalue } $self->part_export_option;
+}
+
+=item option OPTIONNAME
+
+Returns the option value for the given name, or the empty string.
+
+=cut
+
+sub option {
+  my $self = shift;
+  my $part_export_option =
+    qsearchs('part_export_option', {
+      exportnum  => $self->exportnum,
+      optionname => shift,
+  } );
+  $part_export_option ? $part_export_option->optionvalue : '';
+}
+
+=item rebless
+
+Reblesses the object into the FS::part_export::EXPORTTYPE class, where
+EXPORTTYPE is the object's I<exporttype> field.  There should be better docs
+on how to create new exports (and they should live in their own files and be
+autoloaded-on-demand), but until then, see L</NEW EXPORT CLASSES>.
+
+=cut
+
+sub rebless {
+  my $self = shift;
+  my $exporttype = $self->exporttype;
+  my $class = ref($self). "::$exporttype";
+  eval "use $class;";
+  die $@ if $@;
+  bless($self, $class);
+}
+
+=item export_insert SVC_OBJECT
+
+=cut
+
+sub export_insert {
+  my $self = shift;
+  $self->rebless;
+  $self->_export_insert(@_);
+}
+
+#sub AUTOLOAD {
+#  my $self = shift;
+#  $self->rebless;
+#  my $method = $AUTOLOAD;
+#  #$method =~ s/::(\w+)$/::_$1/; #infinite loop prevention
+#  $method =~ s/::(\w+)$/_$1/; #infinite loop prevention
+#  $self->$method(@_);
+#}
+
+=item export_replace NEW OLD
+
+=cut
+
+sub export_replace {
+  my $self = shift;
+  $self->rebless;
+  $self->_export_replace(@_);
+}
+
+=item export_delete
+
+=cut
+
+sub export_delete {
+  my $self = shift;
+  $self->rebless;
+  $self->_export_delete(@_);
+}
+
+=item export_suspend
+
+=cut
+
+sub export_suspend {
+  my $self = shift;
+  $self->rebless;
+  $self->_export_suspend(@_);
+}
+
+=item export_unsuspend
+
+=cut
+
+sub export_unsuspend {
+  my $self = shift;
+  $self->rebless;
+  $self->_export_unsuspend(@_);
+}
+
+#fallbacks providing useful error messages intead of infinite loops
+sub _export_insert {
+  my $self = shift;
+  return "_export_insert: unknown export type ". $self->exporttype;
+}
+
+sub _export_replace {
+  my $self = shift;
+  return "_export_replace: unknown export type ". $self->exporttype;
+}
+
+sub _export_delete {
+  my $self = shift;
+  return "_export_delete: unknown export type ". $self->exporttype;
+}
+
+#fallbacks providing null operations
+
+sub _export_suspend {
+  my $self = shift;
+  #warn "warning: _export_suspened unimplemented for". ref($self);
+  '';
+}
+
+sub _export_unsuspend {
+  my $self = shift;
+  #warn "warning: _export_unsuspend unimplemented for ". ref($self);
+  '';
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item export_info [ SVCDB ]
+
+Returns a hash reference of the exports for the given I<svcdb>, or if no
+I<svcdb> is specified, for all exports.  The keys of the hash are
+I<exporttype>s and the values are again hash references containing information
+on the export:
+
+  'desc'     => 'Description',
+  'options'  => {
+                  'option'  => { label=>'Option Label' },
+                  'option2' => { label=>'Another label' },
+                },
+  'nodomain' => 'Y', #or ''
+  'notes'    => 'Additional notes',
+
+=cut
+
+sub export_info {
+  #warn $_[0];
+  return $exports{$_[0]} if @_;
+  #{ map { %{$exports{$_}} } keys %exports };
+  my $r = { map { %{$exports{$_}} } keys %exports };
+}
+
+#=item exporttype2svcdb EXPORTTYPE
+#
+#Returns the applicable I<svcdb> for an I<exporttype>.
+#
+#=cut
+#
+#sub exporttype2svcdb {
+#  my $exporttype = $_[0];
+#  foreach my $svcdb ( keys %exports ) {
+#    return $svcdb if grep { $exporttype eq $_ } keys %{$exports{$svcdb}};
+#  }
+#  '';
+#}
+
+tie my %sysvshell_options, 'Tie::IxHash',
+  'crypt' => { label=>'Password encryption',
+               type=>'select', options=>[qw(crypt md5)],
+               default=>'crypt',
+             },
+;
+
+tie my %bsdshell_options, 'Tie::IxHash', 
+  'crypt' => { label=>'Password encryption',
+               type=>'select', options=>[qw(crypt md5)],
+               default=>'crypt',
+             },
+;
+
+tie my %shellcommands_options, 'Tie::IxHash',
+  #'machine' => { label=>'Remote machine' },
+  'user' => { label=>'Remote username', default=>'root' },
+  'useradd' => { label=>'Insert command',
+                 default=>'useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username'
+                #default=>'cp -pr /etc/skel $dir; chown -R $uid.$gid $dir'
+               },
+  'useradd_stdin' => { label=>'Insert command STDIN',
+                       type =>'textarea',
+                       default=>'',
+                     },
+  'userdel' => { label=>'Delete command',
+                 default=>'userdel -r $username',
+                 #default=>'rm -rf $dir',
+               },
+  'userdel_stdin' => { label=>'Delete command STDIN',
+                       type =>'textarea',
+                       default=>'',
+                     },
+  'usermod' => { label=>'Modify command',
+                 default=>'usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username',
+                #default=>'[ -d $old_dir ] && mv $old_dir $new_dir || ( '.
+                 #  'chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; '.
+                 #  'find . -depth -print | cpio -pdm $new_dir; '.
+                 #  'chmod u-t $new_dir; chown -R $uid.$gid $new_dir; '.
+                 #  'rm -rf $old_dir'.
+                 #')'
+               },
+  'usermod_stdin' => { label=>'Modify command STDIN',
+                       type =>'textarea',
+                       default=>'',
+                     },
+  'usermod_pwonly' => { label=>'Disallow username changes',
+                        type =>'checkbox',
+                      },
+  'suspend' => { label=>'Suspension command',
+                 default=>'',
+               },
+  'suspend_stdin' => { label=>'Suspension command STDIN',
+                       default=>'',
+                     },
+  'unsuspend' => { label=>'Unsuspension command',
+                   default=>'',
+                 },
+  'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
+                         default=>'',
+                       },
+;
+
+tie my %shellcommands_withdomain_options, 'Tie::IxHash',
+  'user' => { label=>'Remote username', default=>'root' },
+  'useradd' => { label=>'Insert command',
+                 #default=>''
+               },
+  'useradd_stdin' => { label=>'Insert command STDIN',
+                       type =>'textarea',
+                       #default=>"$_password\n$_password\n",
+                     },
+  'userdel' => { label=>'Delete command',
+                 #default=>'',
+               },
+  'userdel_stdin' => { label=>'Delete command STDIN',
+                       type =>'textarea',
+                       #default=>'',
+                     },
+  'usermod' => { label=>'Modify command',
+                 default=>'',
+               },
+  'usermod_stdin' => { label=>'Modify command STDIN',
+                       type =>'textarea',
+                       #default=>"$_password\n$_password\n",
+                     },
+  'usermod_pwonly' => { label=>'Disallow username changes',
+                        type =>'checkbox',
+                      },
+  'suspend' => { label=>'Suspension command',
+                 default=>'',
+               },
+  'suspend_stdin' => { label=>'Suspension command STDIN',
+                       default=>'',
+                     },
+  'unsuspend' => { label=>'Unsuspension command',
+                   default=>'',
+                 },
+  'unsuspend_stdin' => { label=>'Unsuspension command STDIN',
+                         default=>'',
+                       },
+;
+
+tie my %www_shellcommands_options, 'Tie::IxHash',
+  'user' => { label=>'Remote username', default=>'root' },
+  'useradd' => { label=>'Insert command',
+                 default=>'mkdir /var/www/$zone; chown $username /var/www/$zone; ln -s /var/www/$zone $homedir/$zone',
+               },
+  'userdel'  => { label=>'Delete command',
+                  default=>'[ -n &quot;$zone&quot; ] && rm -rf /var/www/$zone; rm $homedir/$zone',
+                },
+  'usermod'  => { label=>'Modify command',
+                  default=>'[ -n &quot;$old_zone&quot; ] && rm $old_homedir/$old_zone; [ &quot;$old_zone&quot; != &quot;$new_zone&quot; -a -n &quot;$new_zone&quot; ] && mv /var/www/$old_zone /var/www/$new_zone; [ &quot;$old_username&quot; != &quot;$new_username&quot; ] && chown -R $new_username /var/www/$new_zone; ln -s /var/www/$new_zone $new_homedir/$new_zone',
+                },
+;
+
+tie my %apache_options, 'Tie::IxHash',
+  'user'       => { label=>'Remote username', default=>'root' },
+  'httpd_conf' => { label=>'httpd.conf snippet location',
+                    default=>'/etc/apache/httpd-freeside.conf', },
+  'template'   => {
+    label   => 'Template',
+    type    => 'textarea',
+    default => <<'END',
+<VirtualHost $domain> #generic
+#<VirtualHost ip.addr> #preferred, http://httpd.apache.org/docs/dns-caveats.html
+DocumentRoot /var/www/$zone
+ServerName $zone
+ServerAlias *.$zone
+#BandWidthModule On
+#LargeFileLimit 4096 12288
+</VirtualHost>
+
+END
+  },
+;
+
+tie my %domain_shellcommands_options, 'Tie::IxHash',
+  'user' => { lable=>'Remote username', default=>'root' },
+  'useradd' => { label=>'Insert command',
+                 default=>'',
+               },
+  'userdel'  => { label=>'Delete command',
+                  default=>'',
+                },
+  'usermod'  => { label=>'Modify command',
+                  default=>'',
+                },
+;
+
+tie my %textradius_options, 'Tie::IxHash',
+  'user' => { label=>'Remote username', default=>'root' },
+  'users' => { label=>'users file location', default=>'/etc/raddb/users' },
+;
+
+tie my %sqlradius_options, 'Tie::IxHash',
+  'datasrc'  => { label=>'DBI data source ' },
+  'username' => { label=>'Database username' },
+  'password' => { label=>'Database password' },
+;
+
+tie my %sqlradius_withdomain_options, 'Tie::IxHash',
+  'datasrc'  => { label=>'DBI data source ' },
+  'username' => { label=>'Database username' },
+  'password' => { label=>'Database password' },
+;
+
+tie my %cyrus_options, 'Tie::IxHash',
+  'server' => { label=>'IMAP server' },
+  'username' => { label=>'Admin username' },
+  'password' => { label=>'Admin password' },
+;
+
+tie my %cp_options, 'Tie::IxHash',
+  'port'      => { label=>'Port number' },
+  'username'  => { label=>'Username' },
+  'password'  => { label=>'Password' },
+  'domain'    => { label=>'Domain' },
+  'workgroup' => { label=>'Default Workgroup' },
+;
+
+tie my %infostreet_options, 'Tie::IxHash',
+  'url'      => { label=>'XML-RPC Access URL', },
+  'login'    => { label=>'InfoStreet login', },
+  'password' => { label=>'InfoStreet password', },
+  'groupID'  => { label=>'InfoStreet groupID', },
+;
+
+tie my %vpopmail_options, 'Tie::IxHash',
+  #'machine' => { label=>'vpopmail machine', },
+  'dir'     => { label=>'directory', }, # ?more info? default?
+  'uid'     => { label=>'vpopmail uid' },
+  'gid'     => { label=>'vpopmail gid' },
+  'restart' => { label=> 'vpopmail restart command',
+                 default=> 'cd /home/vpopmail/domains; for domain in *; do /home/vpopmail/bin/vmkpasswd $domain; done; /var/qmail/bin/qmail-newu; killall -HUP qmail-send',
+               },
+;
+
+tie my %bind_options, 'Tie::IxHash',
+  #'machine'     => { label=>'named machine' },
+  'named_conf'   => { label  => 'named.conf location',
+                      default=> '/etc/bind/named.conf' },
+  'zonepath'     => { label => 'path to zone files',
+                      default=> '/etc/bind/', },
+  'bind_release' => { label => 'ISC BIND Release',
+                      type  => 'select',
+                      options => [qw(BIND8 BIND9)],
+                      default => 'BIND8' },
+  'bind9_minttl' => { label => 'The minttl required by bind9 and RFC1035.',
+                      default => '1D' },
+;
+
+tie my %bind_slave_options, 'Tie::IxHash',
+  #'machine'     => { label=> 'Slave machine' },
+  'master'       => { label=> 'Master IP address(s) (semicolon-separated)' },
+  'named_conf'   => { label   => 'named.conf location',
+                      default => '/etc/bind/named.conf' },
+  'bind_release' => { label => 'ISC BIND Release',
+                      type  => 'select',
+                      options => [qw(BIND8 BIND9)],
+                      default => 'BIND8' },
+  'bind9_minttl' => { label => 'The minttl required by bind9 and RFC1035.',
+                      default => '1D' },
+;
+
+tie my %http_options, 'Tie::IxHash',
+  'method' => { label   =>'Method',
+                type    =>'select',
+                #options =>[qw(POST GET)],
+                options =>[qw(POST)],
+                default =>'POST' },
+  'url'    => { label   => 'URL', default => 'http://', },
+  'insert_data' => {
+    label   => 'Insert data',
+    type    => 'textarea',
+    default => join("\n",
+      'DomainName $svc_x->domain',
+      'Email ( grep { $_ ne "POST" } $svc_x->cust_svc->cust_pkg->cust_main->invoicing_list)[0]',
+      'test 1',
+      'reseller $svc_x->cust_svc->cust_pkg->part_pkg->pkg =~ /reseller/i',
+    ),
+  },
+  'delete_data' => {
+    label   => 'Delete data',
+    type    => 'textarea',
+    default => join("\n",
+    ),
+  },
+  'replace_data' => {
+    label   => 'Replace data',
+    type    => 'textarea',
+    default => join("\n",
+    ),
+  },
+;
+
+tie my %sqlmail_options, 'Tie::IxHash',
+  'datasrc'            => { label => 'DBI data source' },
+  'username'           => { label => 'Database username' },
+  'password'           => { label => 'Database password' },
+  'server_type'        => {
+    label   => 'Server type',
+    type    => 'select',
+    options => [qw(dovecot_plain dovecot_crypt dovecot_digest_md5 courier_plain
+                   courier_crypt)],
+    default => ['dovecot_plain'], },
+  'svc_acct_table'     => { label => 'User Table', default => 'user_acct' },
+  'svc_forward_table'  => { label => 'Forward Table', default => 'forward' },
+  'svc_domain_table'   => { label => 'Domain Table', default => 'domain' },
+  'svc_acct_fields'    => { label => 'svc_acct Export Fields',
+                            default => 'username _password domsvc svcnum' },
+  'svc_forward_fields' => { label => 'svc_forward Export Fields',
+                            default => 'domain svcnum catchall' },
+  'svc_domain_fields'  => { label => 'svc_domain Export Fields',
+                            default => 'srcsvc dstsvc dst' },
+  'resolve_dstsvc'     => { label => q{Resolve svc_forward.dstsvc to an email address and store it in dst. (Doesn't require that you also export dstsvc.)},
+                            type => 'checkbox' },
+
+;
+
+tie my %ldap_options, 'Tie::IxHash',
+  'dn'         => { label=>'Root DN' },
+  'password'   => { label=>'Root DN password' },
+  'userdn'     => { label=>'User DN' },
+  'attributes' => { label=>'Attributes',
+                    type=>'textarea',
+                    default=>join("\n",
+                      'uid $username',
+                      'mail $username\@$domain',
+                      'uidno $uid',
+                      'gidno $gid',
+                      'cn $first',
+                      'sn $last',
+                      'mailquota $quota',
+                      'vmail',
+                      'location',
+                      'mailtag',
+                      'mailhost',
+                      'mailmessagestore $dir',
+                      'userpassword $crypt_password',
+                      'hint',
+                      'answer $sec_phrase',
+                      'objectclass top,person,inetOrgPerson',
+                    ),
+                  },
+  'radius'     => { label=>'Export RADIUS attributes', type=>'checkbox', },
+;
+
+tie my %forward_shellcommands_options, 'Tie::IxHash',
+  'user' => { lable=>'Remote username', default=>'root' },
+  'useradd' => { label=>'Insert command',
+                 default=>'',
+               },
+  'userdel'  => { label=>'Delete command',
+                  default=>'',
+                },
+  'usermod'  => { label=>'Modify command',
+                  default=>'',
+                },
+;
+
+#export names cannot have dashes...
+%exports = (
+  'svc_acct' => {
+    'sysvshell' => {
+      'desc' =>
+        'Batch export of /etc/passwd and /etc/shadow files (Linux/SysV).',
+      'options' => \%sysvshell_options,
+      'nodomain' => 'Y',
+      'notes' => 'MD5 crypt requires installation of <a href="http://search.cpan.org/search?dist=Crypt-PasswdMD5">Crypt::PasswdMD5</a> from CPAN.    Run bin/sysvshell.export to export the files.',
+    },
+    'bsdshell' => {
+      'desc' =>
+        'Batch export of /etc/passwd and /etc/master.passwd files (BSD).',
+      'options' => \%bsdshell_options,
+      'nodomain' => 'Y',
+      'notes' => 'MD5 crypt requires installation of <a href="http://search.cpan.org/search?dist=Crypt-PasswdMD5">Crypt::PasswdMD5</a> from CPAN.  Run bin/bsdshell.export to export the files.',
+    },
+#    'nis' => {
+#      'desc' =>
+#        'Batch export of /etc/global/passwd and /etc/global/shadow for NIS ',
+#      'options' => {},
+#    },
+    'textradius' => {
+      'desc' => 'Real-time export to a text /etc/raddb/users file (Livingston, Cistron)',
+      'options' => \%textradius_options,
+      'notes' => 'This will edit a text RADIUS users file in place on a remote server.  Requires installation of <a href="http://search.cpan.org/search?dist=RADIUS-UserFile">RADIUS::UserFile</a> from CPAN.  If using RADIUS::UserFile 1.01, make sure to apply <a href="http://rt.cpan.org/NoAuth/Bug.html?id=1210">this patch</a>.  Also make sure <a href="http://rsync.samba.org/">rsync</a> is installed on the remote machine, and <a href="../docs/ssh.html">SSH is setup for unattended operation</a>.',
+    },
+
+    'shellcommands' => {
+      'desc' => 'Real-time export via remote SSH (i.e. useradd, userdel, etc.)',
+      'options' => \%shellcommands_options,
+      'nodomain' => 'Y',
+      'notes' => 'Run remote commands via SSH.  Usernames are considered unique (also see shellcommands_withdomain).  You probably want this if the commands you are running will not accept a domain as a parameter.  You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="Linux/NetBSD" onClick=\'this.form.useradd.value = "useradd -c $finger -d $dir -m -s $shell -u $uid -p $crypt_password $username"; this.form.useradd_stdin.value = ""; this.form.userdel.value = "userdel -r $username"; this.form.userdel_stdin.value=""; this.form.usermod.value = "usermod -c $new_finger -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -p $new_crypt_password $old_username"; this.form.usermod_stdin.value = "";\'><LI><INPUT TYPE="button" VALUE="FreeBSD" onClick=\'this.form.useradd.value = "pw useradd $username -d $dir -m -s $shell -u $uid -g $gid -c $finger -h 0"; this.form.useradd_stdin.value = "$_password\n"; this.form.userdel.value = "pw userdel $username -r"; this.form.userdel_stdin.value=""; this.form.usermod.value = "pw usermod $old_username -d $new_dir -m -l $new_username -s $new_shell -u $new_uid -c $new_finger -h 0"; this.form.usermod_stdin.value = "$new__password\n";\'><LI><INPUT TYPE="button" VALUE="Just maintain directories (use with sysvshell or bsdshell)" onClick=\'this.form.useradd.value = "cp -pr /etc/skel $dir; chown -R $uid.$gid $dir"; this.form.useradd_stdin.value = ""; this.form.usermod.value = "[ -d $old_dir ] && mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $new_uid.$new_gid $new_dir; rm -rf $old_dir )"; this.form.usermod_stdin.value = ""; this.form.userdel.value = "rm -rf $dir"; this.form.userdel_stdin.value="";\'></UL>The following variables are available for interpolation (prefixed with new_ or old_ for replace operations): <UL><LI><code>$username</code><LI><code>$_password</code><LI><code>$quoted_password</code> - unencrypted password quoted for the shell<LI><code>$crypt_password</code> - encrypted password<LI><code>$uid</code><LI><code>$gid</code><LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)<LI><code>$dir</code> - home directory<LI><code>$shell</code><LI><code>$quota</code><LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.</UL>',
+    },
+
+    'shellcommands_withdomain' => {
+      'desc' => 'Real-time export via remote SSH (vpopmail, etc.).',
+      'options' => \%shellcommands_withdomain_options,
+      'notes' => 'Run remote commands via SSH.  username@domain (rather than just usernames) are considered unique (also see shellcommands).  You probably want this if the commands you are running will accept a domain as a parameter, and will allow the same username with different domains.  You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="vpopmail" onClick=\'this.form.useradd.value = "/home/vpopmail/bin/vadduser $username\\\@$domain $quoted_password"; this.form.useradd_stdin.value = ""; this.form.userdel.value = "/home/vpopmail/bin/vdeluser $username\\\@$domain"; this.form.userdel_stdin.value=""; this.form.usermod.value = "/home/vpopmail/bin/vpasswd $new_username\\\@$new_domain $new_quoted_password"; this.form.usermod_stdin.value = ""; this.form.usermod_pwonly.checked = true;\'></UL>The following variables are available for interpolation (prefixed with <code>new_</code> or <code>old_</code> for replace operations): <UL><LI><code>$username</code><LI><code>$domain</code><LI><code>$_password</code><LI><code>$quoted_password</code> - unencrypted password quoted for the shell<LI><code>$crypt_password</code> - encrypted password<LI><code>$uid</code><LI><code>$gid</code><LI><code>$finger</code> - GECOS, already quoted for the shell (do not add additional quotes)<LI><code>$dir</code> - home directory<LI><code>$shell</code><LI><code>$quota</code><LI>All other fields in <a href="../docs/schema.html#svc_acct">svc_acct</a> are also available.</UL>',
+    },
+
+    'ldap' => {
+      'desc' => 'Real-time export to LDAP',
+      'options' => \%ldap_options,
+      'notes' => 'Real-time export to arbitrary LDAP attributes.  Requires installation of <a href="http://search.cpan.org/search?dist=Net-LDAP">Net::LDAP</a> from CPAN.',
+    },
+
+    'sqlradius' => {
+      'desc' => 'Real-time export to SQL-backed RADIUS (ICRADIUS, FreeRADIUS)',
+      'options' => \%sqlradius_options,
+      'nodomain' => 'Y',
+      'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a> or <a href="http://radius.innercite.com/">ICRADIUS</a>.  This export does not export RADIUS realms (see also sqlradius_withdomain).  An existing RADIUS database will be updated in realtime, but you can use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete the entire RADIUS database and repopulate the tables from the Freeside database.  See the <a href="http://search.cpan.org/doc/TIMB/DBI/DBI.pm">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source.',
+    },
+
+    'sqlradius_withdomain' => {
+      'desc' => 'Real-time export to SQL-backed RADIUS (ICRADIUS, FreeRADIUS) with realms',
+      'options' => \%sqlradius_withdomain_options,
+      'nodomain' => '',
+      'notes' => 'Real-time export of radcheck, radreply and usergroup tables to any SQL database for <a href="http://www.freeradius.org/">FreeRADIUS</a> or <a href="http://radius.innercite.com/">ICRADIUS</a>.  This export exports domains to RADIUS realms (see also sqlradius).  An existing RADIUS database will be updated in realtime, but you can use <a href="../docs/man/bin/freeside-sqlradius-reset">freeside-sqlradius-reset</a> to delete the entire RADIUS database and repopulate the tables from the Freeside database.  See the <a href="http://search.cpan.org/doc/TIMB/DBI/DBI.pm">DBI documentation</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">documentation for your DBD</a> for the exact syntax of a DBI data source.',
+    },
+
+    'sqlmail' => {
+      'desc' => 'Real-time export to SQL-backed mail server',
+      'options' => \%sqlmail_options,
+      'nodomain' => '',
+      'notes' => 'Database schema can be made to work with Courier IMAP and Exim.  Others could work but are untested. (...extended description from pc-intouch?...)',
+    },
+
+    'cyrus' => {
+      'desc' => 'Real-time export to Cyrus IMAP server',
+      'options' => \%cyrus_options,
+      'nodomain' => 'Y',
+      'notes' => 'Integration with <a href="http://asg.web.cmu.edu/cyrus/imapd/">Cyrus IMAP Server</a>.  Cyrus::IMAP::Admin should be installed locally and the connection to the server secured.  <B>svc_acct.quota</B>, if available, is used to set the Cyrus quota. '
+    },
+
+    'cp' => {
+      'desc' => 'Real-time export to Critical Path Account Provisioning Protocol',
+      'options' => \%cp_options,
+      'notes' => 'Real-time export to <a href="http://www.cp.net/">Critial Path Account Provisioning Protocol</a>.  Requires installation of <a href="http://search.cpan.org/search?dist=Net-APP">Net::APP</a> from CPAN.',
+    },
+    
+    'infostreet' => {
+      'desc' => 'Real-time export to InfoStreet streetSmartAPI',
+      'options' => \%infostreet_options,
+      'nodomain' => 'Y',
+      'notes' => 'Real-time export to <a href="http://www.infostreet.com/">InfoStreet</a> streetSmartAPI.  Requires installation of <a href="http://search.cpan.org/search?dist=Frontier-Client">Frontier::Client</a> from CPAN.',
+    },
+
+    'vpopmail' => {
+      'desc' => 'Real-time export to vpopmail text files',
+      'options' => \%vpopmail_options,
+      'notes' => 'Real time export to <a href="http://inter7.com/vpopmail/">vpopmail</a> text files.  <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed, and you will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a> to <b>vpopmail</b>@<i>export.host</i>.',
+    },
+
+  },
+
+  'svc_domain' => {
+
+    'bind' => {
+      'desc' =>'Batch export to BIND named',
+      'options' => \%bind_options,
+      'notes' => 'Batch export of BIND zone and configuration files to primary nameserver.  <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed.  Run bin/bind.export to export the files.',
+    },
+
+    'bind_slave' => {
+      'desc' =>'Batch export to slave BIND named',
+      'options' => \%bind_slave_options,
+      'notes' => 'Batch export of BIND configuration file to a secondary nameserver.  Zones are slaved from the listed masters.  <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed.  Run bin/bind.export to export the files.',
+    },
+
+    'http' => {
+      'desc' => 'Send an HTTP or HTTPS GET or POST request',
+      'options' => \%http_options,
+      'notes' => 'Send an HTTP or HTTPS GET or POST to the specified URL.  <a href="http://search.cpan.org/search?dist=libwww-perl">libwww-perl</a> must be installed.  For HTTPS support, <a href="http://search.cpan.org/search?dist=Crypt-SSLeay">Crypt::SSLeay</a> or <a href="http://search.cpan.org/search?dist=IO-Socket-SSL">IO::Socket::SSL</a> is required.',
+    },
+
+    'sqlmail' => {
+      'desc' => 'Real-time export to SQL-backed mail server',
+      'options' => \%sqlmail_options,
+      #'nodomain' => 'Y',
+      'notes' => 'Database schema can be made to work with Courier IMAP and Exim.  Others could work but are untested. (...extended description from pc-intouch?...)',
+    },
+
+    'domain_shellcommands' => {
+      'desc' => 'Run remote commands via SSH, for domains.',
+      'options' => \%domain_shellcommands_options,
+      'notes'    => 'Run remote commands via SSH, for domains.  You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="qmail catchall .qmail-domain-default maintenance" onClick=\'this.form.useradd.value = "[ \"$uid\" -a \"$gid\" -a \"$dir\" -a \"$qdomain\" ] && [ -e $dir/.qmail-$qdomain-default ] || { touch $dir/.qmail-$qdomain-default; chown $uid:$gid $dir/.qmail-$qdomain-default; }"; this.form.userdel.value = ""; this.form.usermod.value = "";\'></UL>The following variables are available for interpolation (prefixed with <code>new_</code> or <code>old_</code> for replace operations): <UL><LI><code>$domain</code><LI><code>$qdomain</code> - domain with periods replaced by colons<LI><code>$uid</code> - of catchall account<LI><code>$gid</code> - of catchall account<LI><code>$dir</code> - home directory of catchall account<LI>All other fields in <a href="../docs/schema.html#svc_domain">svc_domain</a> are also available.</UL>',
+    },
+
+
+  },
+
+  'svc_forward' => {
+    'sqlmail' => {
+      'desc' => 'Real-time export to SQL-backed mail server',
+      'options' => \%sqlmail_options,
+      #'nodomain' => 'Y',
+      'notes' => 'Database schema can be made to work with Courier IMAP and Exim.  Others could work but are untested. (...extended description from pc-intouch?...)',
+    },
+
+    'forward_shellcommands' => {
+      'desc' => 'Run remote commands via SSH, for forwards',
+      'options' => \%forward_shellcommands_options,
+      'notes' => 'Run remote commands via SSH, for forwards.  You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>Use these buttons for some useful presets:<UL><LI><INPUT TYPE="button" VALUE="text vpopmail maintenance" onClick=\'this.form.useradd.value = "[ -d /home/vpopmail/domains/$domain/$username ] && { echo \"$destination\" > /home/vpopmail/domains/$domain/$username/.qmail; chown vpopmail:vchkpw /home/vpopmail/domains/$domain/$username/.qmail; }"; this.form.userdel.value = "rm /home/vpopmail/domains/$domain/$username/.qmail"; this.form.usermod.value = "mv /home/vpopmail/domains/$old_domain/$old_username/.qmail /home/vpopmail/domains/$new_domain/$new_username; [ \"$old_destination\" != \"$new_destination\" ] && { echo \"$new_destination\" > /home/vpopmail/domains/$new_domain/$new_username/.qmail; chown vpopmail:vchkpw /home/vpopmail/domains/$new_domain/$new_username/.qmail; }";\'></UL>The following variables are available for interpolation (prefixed with <code>new_</code> or <code>old_</code> for replace operations): <UL><LI><code>$username</code><LI><code>$domain</code><LI><code>$destination</code> - forward destination<LI>All other fields in <a href="../docs/schema.html#svc_forward">svc_forward</a> are also available.</UL>',
+    },
+  },
+
+  'svc_www' => {
+    'www_shellcommands' => {
+      'desc' => 'Run remote commands via SSH, for virtual web sites.',
+      'options' => \%www_shellcommands_options,
+      'notes'    => 'Run remote commands via SSH, for virtual web sites.  You will need to <a href="../docs/ssh.html">setup SSH for unattended operation</a>.<BR><BR>The following variables are available for interpolation (prefixed with <code>new_</code> or <code>old_</code> for replace operations): <UL><LI><code>$zone</code><LI><code>$username</code><LI><code>$homedir</code><LI>All other fields in <a href="../docs/schema.html#svc_www">svc_www</a> are also available.</UL>',
+    },
+
+    'apache' => {
+      'desc' => 'Export an Apache httpd.conf file snippet.',
+      'options' => \%apache_options,
+      'notes' => 'Batch export of an httpd.conf snippet from a template.  Typically used with something like <code>Include /etc/apache/httpd-freeside.conf</code> in httpd.conf.  <a href="http://search.cpan.org/search?dist=File-Rsync">File::Rsync</a> must be installed.  Run bin/apache.export to export the files.',
+    },
+  },
+
+  'svc_broadband' => {
+  },
+
+);
+
+=back
+
+=head1 NEW EXPORT CLASSES
+
+Should be added to the %export hash here, and a module should be added in
+FS/FS/part_export/ (an example may be found in eg/export_template.pm)
+
+=head1 BUGS
+
+All the stuff in the %exports hash should be generated from the specific
+export modules.
+
+Hmm... cust_export class (not necessarily a database table...) ... ?
+
+deprecated column...
+
+=head1 SEE ALSO
+
+L<FS::part_export_option>, L<FS::export_svc>, L<FS::svc_acct>,
+L<FS::svc_domain>,
+L<FS::svc_forward>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_export/apache.pm b/FS/FS/part_export/apache.pm
new file mode 100644 (file)
index 0000000..9161d72
--- /dev/null
@@ -0,0 +1,7 @@
+package FS::part_export::apache;
+
+use vars qw(@ISA);
+use FS::part_export::null;
+
+@ISA = qw(FS::part_export::null);
+
diff --git a/FS/FS/part_export/bind.pm b/FS/FS/part_export/bind.pm
new file mode 100644 (file)
index 0000000..b72c9bd
--- /dev/null
@@ -0,0 +1,7 @@
+package FS::part_export::bind;
+
+use vars qw(@ISA);
+use FS::part_export::null;
+
+@ISA = qw(FS::part_export::null);
+
diff --git a/FS/FS/part_export/bind_slave.pm b/FS/FS/part_export/bind_slave.pm
new file mode 100644 (file)
index 0000000..ebb29c1
--- /dev/null
@@ -0,0 +1,7 @@
+package FS::part_export::bind_slave;
+
+use vars qw(@ISA);
+use FS::part_export::null;
+
+@ISA = qw(FS::part_export::null);
+
diff --git a/FS/FS/part_export/bsdshell.pm b/FS/FS/part_export/bsdshell.pm
new file mode 100644 (file)
index 0000000..0664209
--- /dev/null
@@ -0,0 +1,7 @@
+package FS::part_export::bsdshell;
+
+use vars qw(@ISA);
+use FS::part_export::null;
+
+@ISA = qw(FS::part_export::null);
+
diff --git a/FS/FS/part_export/cp.pm b/FS/FS/part_export/cp.pm
new file mode 100644 (file)
index 0000000..c4750dd
--- /dev/null
@@ -0,0 +1,136 @@
+package FS::part_export::cp;
+
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->cp_queue( $svc_acct->svcnum, 'create_mailbox',
+    'Mailbox'   => $svc_acct->username,
+    'Password'  => $svc_acct->_password,
+    'Workgroup' => $self->option('workgroup'),
+    'Domain'    => $svc_acct->domain,
+  );
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+  return "can't change domain with Critical Path"
+    if $old->domain ne $new->domain;
+  return '' unless $old->username  ne $new->username
+                || $old->_password ne $new->_password;
+  $self->cp_queue( $new->svcnum, 'replace', $new->domain,
+    $old->username, $new->username, $old->_password, $new->_password );
+}
+
+sub _export_delete {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->cp_queue( $svc_acct->svcnum, 'delete_mailbox',
+    'Mailbox'   => $svc_acct->username,
+    'Domain'    => $svc_acct->domain,
+  );
+}
+
+sub _export_suspend {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->cp_queue( $svc_acct->svcnum, 'set_mailbox_status',
+    'Mailbox'       => $svc_acct->username,
+    'Domain'        => $svc_acct->domain,
+    'OTHER'         => 'T',
+    'OTHER_SUSPEND' => 'T',
+  );
+}
+
+sub _export_unsuspend {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->cp_queue( $svc_acct->svcnum, 'set_mailbox_status',
+    'Mailbox'       => $svc_acct->username,
+    'Domain'        => $svc_acct->domain,
+    'PAYMENT'       => 'F',
+    'OTHER'         => 'F',
+    'OTHER_SUSPEND' => 'F',
+    'OTHER_BOUNCE'  => 'F',
+  );
+}
+
+sub cp_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => 'FS::part_export::cp::cp_command',
+  };
+  $queue->insert(
+    $self->machine,
+    $self->option('port'),
+    $self->option('username'),
+    $self->option('password'),
+    $self->option('domain'),
+    $method,
+    @_,
+  );
+}
+
+sub cp_command { #subroutine, not method
+  my($host, $port, $username, $password, $login_domain, $method, @args) = @_;
+
+  #quelle hack
+  if ( $method eq 'replace' ) {
+  
+    my( $domain, $old_username, $new_username, $old_password, $new_password)
+      = @args;
+
+    if ( $old_username ne $new_username ) {
+      cp_command($host, $port, $username, $password, 'rename_mailbox',
+        Domain        => $domain,
+        Old_Mailbox   => $old_username,
+        New_Mailbox   => $new_username,
+      );
+    }
+
+    #my $other = 'F';
+    if ( $new_password =~ /^\*SUSPENDED\* (.*)$/ ) {
+      $new_password = $1;
+    #  $other = 'T';
+    }
+    #cp_command($host, $port, $username, $password, $login_domain,
+    #  'set_mailbox_status',
+    #  Domain       => $domain,
+    #  Mailbox      => $new_username,
+    #  Other        => $other,
+    #  Other_Bounce => $other,
+    #);
+
+    if ( $old_password ne $new_password ) {
+      cp_command($host, $port, $username, $password, $login_domain,
+        'change_mailbox',
+        Domain    => $domain,
+        Mailbox   => $new_username,
+        Password  => $new_password,
+      );
+    }
+
+    return;
+  }
+  #eof quelle hack
+
+  eval "use Net::APP;";
+
+  my $app = new Net::APP (
+    "$host:$port",
+    User     => $username,
+    Password => $password,
+    Domain   => $login_domain,
+    Timeout  => 60,
+    #Debug    => 1,
+  ) or die "$@\n";
+
+  $app->$method( @args );
+
+  die $app->message."\n" unless $app->ok;
+
+}
+
diff --git a/FS/FS/part_export/cyrus.pm b/FS/FS/part_export/cyrus.pm
new file mode 100644 (file)
index 0000000..110ff19
--- /dev/null
@@ -0,0 +1,98 @@
+package FS::part_export::cyrus;
+
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self, $svc_acct) = (shift, shift);
+  $self->cyrus_queue( $svc_acct->svcnum, 'insert',
+    $svc_acct->username, $svc_acct->quota );
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+  return "can't change username using Cyrus"
+    if $old->username ne $new->username;
+  return '';
+#  #return '' unless $old->_password ne $new->_password;
+#  $self->cyrus_queue( $new->svcnum,
+#    'replace', $new->username, $new->_password );
+}
+
+sub _export_delete {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->cyrus_queue( $svc_acct->svcnum, 'delete',
+    $svc_acct->username );
+}
+
+#a good idea to queue anything that could fail or take any time
+sub cyrus_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::cyrus::cyrus_$method",
+  };
+  $queue->insert(
+    $self->option('server'),
+    $self->option('username'),
+    $self->option('password'),
+    @_
+  );
+}
+
+sub cyrus_insert { #subroutine, not method
+  my $client = cyrus_connect(shift, shift, shift);
+  my( $username, $quota ) = @_;
+  my $rc = $client->create("user.$username");
+  my $error = $client->error;
+  die "creating user.$username: $error" if $error;
+
+  $rc = $client->setacl("user.$username", $username => 'all' );
+  $error = $client->error;
+  die "setacl user.$username: $error" if $error;
+
+  if ( $quota ) {
+    $rc = $client->setquota("user.$username", 'STORAGE' => $quota );
+    $error = $client->error;
+    die "setquota user.$username: $error" if $error;
+  }
+
+}
+
+sub cyrus_delete { #subroutine, not method
+  my ( $server, $admin_username, $password_username, $username ) = @_;
+  my $client = cyrus_connect($server, $admin_username, $password_username);
+
+  my $rc = $client->setacl("user.$username", $admin_username => 'all' );
+  my $error = $client->error;
+  die $error if $error;
+
+  $rc = $client->delete("user.$username");
+  $error = $client->error;
+  die $error if $error;
+}
+
+sub cyrus_connect {
+
+  my( $server, $admin_username, $admin_password ) = @_;
+
+  eval "use Cyrus::IMAP::Admin;";
+
+  my $client = Cyrus::IMAP::Admin->new($server);
+  $client->authenticate(
+    -user      => $admin_username,
+    -mechanism => "login",       
+    -password  => $admin_password,
+  );
+  $client;
+
+}
+
+#sub cyrus_replace { #subroutine, not method
+#}
+
+
diff --git a/FS/FS/part_export/domain_shellcommands.pm b/FS/FS/part_export/domain_shellcommands.pm
new file mode 100644 (file)
index 0000000..0edbab0
--- /dev/null
@@ -0,0 +1,109 @@
+package FS::part_export::domain_shellcommands;
+
+use strict;
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self) = shift;
+  $self->_export_command('useradd', @_);
+}
+
+sub _export_delete {
+  my($self) = shift;
+  $self->_export_command('userdel', @_);
+}
+
+sub _export_command {
+  my ( $self, $action, $svc_domain) = (shift, shift, shift);
+  my $command = $self->option($action);
+
+  #set variable for the command
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${$_} = $svc_domain->getfield($_) foreach $svc_domain->fields;
+  }
+  ( $qdomain = $domain ) =~ s/\./:/g; #see dot-qmail(5): EXTENSION ADDRESSES
+
+  if ( $svc_domain->catchall ) {
+    no strict 'refs';
+    my $svc_acct = $svc_domain->catchall_svc_acct;
+    ${$_} = $svc_acct->getfield($_) foreach qw(uid gid dir);
+  } else {
+    ${$_} = '' foreach qw(uid gid dir);
+  }
+
+  #done setting variables for the command
+
+  $self->shellcommands_queue( $svc_domain->svcnum,
+    user         => $self->option('user')||'root',
+    host         => $self->machine,
+    command      => eval(qq("$command")),
+  );
+}
+
+sub _export_replace {
+  my($self, $new, $old ) = (shift, shift, shift);
+  my $command = $self->option('usermod');
+  
+  #set variable for the command
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${"old_$_"} = $old->getfield($_) foreach $old->fields;
+    ${"new_$_"} = $new->getfield($_) foreach $new->fields;
+  }
+  ( $old_qdomain = $old_domain ) =~ s/\./:/g; #see dot-qmail(5): EXTENSION ADDRESSES
+  ( $new_qdomain = $new_domain ) =~ s/\./:/g; #see dot-qmail(5): EXTENSION ADDRESSES
+
+  if ( $old->catchall ) {
+    no strict 'refs';
+    my $svc_acct = $old->catchall_svc_acct;
+    ${"old_$_"} = $svc_acct->getfield($_) foreach qw(uid gid dir);
+  } else {
+    ${"old_$_"} = '' foreach qw(uid gid dir);
+  }
+  if ( $new->catchall ) {
+    no strict 'refs';
+    my $svc_acct = $new->catchall_svc_acct;
+    ${"new_$_"} = $svc_acct->getfield($_) foreach qw(uid gid dir);
+  } else {
+    ${"new_$_"} = '' foreach qw(uid gid dir);
+  }
+
+  #done setting variables for the command
+
+  $self->shellcommands_queue( $new->svcnum,
+    user         => $self->option('user')||'root',
+    host         => $self->machine,
+    command      => eval(qq("$command")),
+  );
+}
+
+#a good idea to queue anything that could fail or take any time
+sub shellcommands_queue {
+  my( $self, $svcnum ) = (shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::domain_shellcommands::ssh_cmd",
+  };
+  $queue->insert( @_ );
+}
+
+sub ssh_cmd { #subroutine, not method
+  use Net::SSH '0.07';
+  &Net::SSH::ssh_cmd( { @_ } );
+}
+
+#sub shellcommands_insert { #subroutine, not method
+#}
+#sub shellcommands_replace { #subroutine, not method
+#}
+#sub shellcommands_delete { #subroutine, not method
+#}
+
diff --git a/FS/FS/part_export/forward_shellcommands.pm b/FS/FS/part_export/forward_shellcommands.pm
new file mode 100644 (file)
index 0000000..f6fcb60
--- /dev/null
@@ -0,0 +1,110 @@
+package FS::part_export::forward_shellcommands;
+
+use strict;
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self) = shift;
+  $self->_export_command('useradd', @_);
+}
+
+sub _export_delete {
+  my($self) = shift;
+  $self->_export_command('userdel', @_);
+}
+
+sub _export_command {
+  my ( $self, $action, $svc_forward ) = (shift, shift, shift);
+  my $command = $self->option($action);
+
+  #set variable for the command
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${$_} = $svc_forward->getfield($_) foreach $svc_forward->fields;
+  }
+
+  my $svc_acct = $svc_forward->srcsvc_acct;
+  $username = $svc_acct->username;
+  $domain = $svc_acct->domain;
+  if ($svc_forward->dstsvc_acct) {
+    $destination = $svc_forward->dstsvc_acct->email;
+  } else {
+    $destination = $svc_forward->dst;
+  }
+
+  #done setting variables for the command
+
+  $self->shellcommands_queue( $svc_forward->svcnum,
+    user         => $self->option('user')||'root',
+    host         => $self->machine,
+    command      => eval(qq("$command")),
+  );
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+  my $command = $self->option('usermod');
+  
+  #set variable for the command
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${"old_$_"} = $old->getfield($_) foreach $old->fields;
+    ${"new_$_"} = $new->getfield($_) foreach $new->fields;
+  }
+
+  my $old_svc_acct = $old->srcsvc_acct;
+  $old_username = $old_svc_acct->username;
+  $old_domain = $old_svc_acct->domain;
+  if ($old->dstsvc_acct) {
+    $old_destination = $old->dstsvc_acct->email;
+  } else {
+    $old_destination = $old->dst;
+  }
+
+  my $new_svc_acct = $new->srcsvc_acct;
+  $new_username = $new_svc_acct->username;
+  $new_domain = $new_svc_acct->domain;
+  if ($new->dstsvc) {
+    $new_destination = $new->dstsvc_acct->email;
+  } else {
+    $new_destination = $new->dst;
+  }
+
+  #done setting variables for the command
+
+  $self->shellcommands_queue( $new->svcnum,
+    user         => $self->option('user')||'root',
+    host         => $self->machine,
+    command      => eval(qq("$command")),
+  );
+}
+
+#a good idea to queue anything that could fail or take any time
+sub shellcommands_queue {
+  my( $self, $svcnum ) = (shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::forward_shellcommands::ssh_cmd",
+  };
+  $queue->insert( @_ );
+}
+
+sub ssh_cmd { #subroutine, not method
+  use Net::SSH '0.07';
+  &Net::SSH::ssh_cmd( { @_ } );
+}
+
+#sub shellcommands_insert { #subroutine, not method
+#}
+#sub shellcommands_replace { #subroutine, not method
+#}
+#sub shellcommands_delete { #subroutine, not method
+#}
+
diff --git a/FS/FS/part_export/http.pm b/FS/FS/part_export/http.pm
new file mode 100644 (file)
index 0000000..0e02f0f
--- /dev/null
@@ -0,0 +1,88 @@
+package FS::part_export::http;
+
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my $self = shift;
+  $self->_export_command('insert', @_);
+}
+
+sub _export_delete {
+  my $self = shift;
+  $self->_export_command('delete', @_);
+}
+
+sub _export_command {
+  my( $self, $action, $svc_x ) = ( shift, shift, shift );
+
+  return unless $self->option("${action}_data");
+
+  $self->http_queue( $svc_x->svcnum,
+    $self->option('method'),
+    $self->option('url'),
+    map {
+      /^\s*(\S+)\s+(.*)$/ or /()()/;
+      my( $field, $value_expression ) = ( $1, $2 );
+      my $value = eval $value_expression;
+      die $@ if $@;
+      ( $field, $value );
+    } split(/\n/, $self->option("${action}_data") )
+  );
+
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = ( shift, shift, shift );
+
+  return unless $self->option('replace_data');
+
+  $self->http_queue( $svc_x->svcnum,
+    $self->option('method'),
+    $self->option('url'),
+    map {
+      /^\s*(\S+)\s+(.*)$/ or /()()/;
+      my( $field, $value_expression ) = ( $1, $2 );
+      die $@ if $@;
+      ( $field, $value );
+    } split(/\n/, $self->option('replace_data') )
+  );
+
+}
+
+sub http_queue {
+  my($self, $svcnum) = (shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::http::http",
+  };
+  $queue->insert( @_ );
+}
+
+sub http {
+  my($method, $url, @data) = @_;
+
+  $method = lc($method);
+
+  eval "use LWP::UserAgent;";
+  die "using LWP::UserAgent: $@" if $@;
+  eval "use HTTP::Request::Common;";
+  die "using HTTP::Request::Common: $@" if $@;
+
+  my $ua = LWP::UserAgent->new;
+
+  #my $response = $ua->$method(
+  #  $url, \%data,
+  #  'Content-Type'=>'application/x-www-form-urlencoded'
+  #);
+  my $req = HTTP::Request::Common::POST( $url, \@data );
+  my $response = $ua->request($req);
+
+  die $response->error_as_HTML if $response->is_error;
+
+}
+
diff --git a/FS/FS/part_export/infostreet.pm b/FS/FS/part_export/infostreet.pm
new file mode 100644 (file)
index 0000000..caca7c5
--- /dev/null
@@ -0,0 +1,255 @@
+package FS::part_export::infostreet;
+
+use vars qw(@ISA %infostreet2cust_main $DEBUG);
+use FS::UID qw(dbh);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+$DEBUG = 0;
+
+%infostreet2cust_main = (
+  'firstName'   => 'first',
+  'lastName'    => 'last',
+  'address1'    => 'address1',
+  'address2'    => 'address2',
+  'city'        => 'city',
+  'state'       => 'state',
+  'zipCode'     => 'zip',
+  'country'     => 'country',
+  'phoneNumber' => 'daytime',
+  'faxNumber'   => 'night', #noment-request...
+);
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my( $self, $svc_acct ) = (shift, shift);
+  my $cust_main = $svc_acct->cust_svc->cust_pkg->cust_main;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $err_or_queue = $self->infostreet_err_or_queue( $svc_acct->svcnum,
+    'createUser', $svc_acct->username, $svc_acct->_password );
+  return $err_or_queue unless ref($err_or_queue);
+  my $jobnum = $err_or_queue->jobnum;
+
+  my %contact_info = ( map {
+    $_ => $cust_main->getfield( $infostreet2cust_main{$_} );
+  } keys %infostreet2cust_main );
+
+  my @emails = grep { $_ ne 'POST' } $cust_main->invoicing_list;
+  $contact_info{'email'} = $emails[0] if @emails;
+
+  #this one is kinda noment-specific
+  $contact_info{'organization'} = $cust_main->agent->agent;
+
+  $err_or_queue = $self->infostreet_queueContact( $svc_acct->svcnum,
+    $svc_acct->username, %contact_info );
+  return $err_or_queue unless ref($err_or_queue);
+
+  # If a quota has been specified set the quota because it is not the default
+  $err_or_queue = $self->infostreet_queueSetQuota( $svc_acct->svcnum, 
+    $svc_acct->username, $svc_acct->quota ) if $svc_acct->quota;
+  return $err_or_queue unless ref($err_or_queue);
+
+  my $error = $err_or_queue->depend_insert( $jobnum );
+  return $error if $error;
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+  return "can't change username with InfoStreet"
+    if $old->username ne $new->username;
+
+  # If the quota has changed then do the export to setQuota
+  my $err_or_queue = $self->infostreet_queueSetQuota( $new->svcnum, $new->username, $new->quota ) 
+        if ( $old->quota != $new->quota );  
+  return $err_or_queue unless ref($err_or_queue);
+
+
+  return '' unless $old->_password ne $new->_password;
+  $self->infostreet_queue( $new->svcnum,
+    'passwd', $new->username, $new->_password );
+}
+
+sub _export_delete {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->infostreet_queue( $svc_acct->svcnum,
+    'purgeAccount,releaseUsername', $svc_acct->username );
+}
+
+sub _export_suspend {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->infostreet_queue( $svc_acct->svcnum,
+    'setStatus', $svc_acct->username, 'DISABLED' );
+}
+
+sub _export_unsuspend {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->infostreet_queue( $svc_acct->svcnum,
+    'setStatus', $svc_acct->username, 'ACTIVE' );
+}
+
+sub infostreet_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => 'FS::part_export::infostreet::infostreet_command',
+  };
+  $queue->insert(
+    $self->option('url'),
+    $self->option('login'),
+    $self->option('password'),
+    $self->option('groupID'),
+    $method,
+    @_,
+  );
+}
+
+#ick false laziness
+sub infostreet_err_or_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => 'FS::part_export::infostreet::infostreet_command',
+  };
+  $queue->insert(
+    $self->option('url'),
+    $self->option('login'),
+    $self->option('password'),
+    $self->option('groupID'),
+    $method,
+    @_,
+  ) or $queue;
+}
+
+sub infostreet_queueContact {
+  my( $self, $svcnum ) = (shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => 'FS::part_export::infostreet::infostreet_setContact',
+  };
+  $queue->insert(
+    $self->option('url'),
+    $self->option('login'),
+    $self->option('password'),
+    $self->option('groupID'),
+    @_,
+  ) or $queue;
+}
+
+sub infostreet_setContact {
+  my($url, $is_username, $is_password, $groupID, $username, %contact_info) = @_;
+  my $accountID = infostreet_command($url, $is_username, $is_password, $groupID,
+    'getAccountID', $username);
+  foreach my $field ( keys %contact_info ) {
+    infostreet_command($url, $is_username, $is_password, $groupID,
+      'setContactField', [ 'int'=>$accountID ], $field, $contact_info{$field} );
+  }
+
+}
+
+sub infostreet_queueSetQuota {
+
+ my( $self, $svcnum) = (shift, shift);
+ my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => 'FS::part_export::infostreet::infostreet_setQuota',
+ };
+
+ $queue->insert(
+    $self->option('url'),
+    $self->option('login'),
+    $self->option('password'),
+    $self->option('groupID'),
+    @_,
+ ) or $queue;
+
+}
+
+sub infostreet_setQuota {
+  my($url, $is_username, $is_password, $groupID, $username, $quota) = @_;
+  infostreet_command($url, $is_username, $is_password, $groupID, 'setQuota', $username, [ 'int'=> $quota ]  );
+}
+
+
+sub infostreet_command { #subroutine, not method
+  my($url, $username, $password, $groupID, $method, @args) = @_;
+
+  warn "[FS::part_export::infostreet] $method ".join(' ', @args)."\n" if $DEBUG;
+
+  #quelle hack
+  if ( $method =~ /,/ ) {
+    foreach my $part ( split(/,\s*/, $method) ) {
+      infostreet_command($url, $username, $password, $groupID, $part, @args);
+    }
+    return;
+  }
+
+  eval "use Frontier::Client;";
+  die $@ if $@;
+
+  eval 'sub Frontier::RPC2::String::repr {
+    my $self = shift;
+    my $value = $$self;
+    $value =~ s/([&<>\"])/$Frontier::RPC2::char_entities{$1}/ge;
+    $value;
+  }';
+  die $@ if $@;
+
+  my $conn = Frontier::Client->new( url => $url );
+  my $key_result = $conn->call( 'authenticate', $username, $password, $groupID);
+  my %key_result = _infostreet_parse($key_result);
+  die $key_result{error} unless $key_result{success};
+  my $key = $key_result{data};
+
+  #my $result = $conn->call($method, $key, @args);
+  my $result = $conn->call( $method, $key,
+                            map {
+                                  if ( ref($_) ) {
+                                    my( $type, $value) = @{$_};
+                                    $conn->$type($value);
+                                  } else {
+                                    $conn->string($_);
+                                  }
+                                } @args );
+  my %result = _infostreet_parse($result);
+  die $result{error} unless $result{success};
+
+  $result->{data};
+
+}
+
+#sub infostreet_command_byid { #subroutine, not method;
+#  my($url, $username, $password, $groupID, $method, @args ) = @_;
+#
+#  infostreet_command
+#
+#}
+
+sub _infostreet_parse { #subroutine, not method
+  my $arg = shift;
+  map {
+    my $value = $arg->{$_};
+    #warn ref($value);
+    $value = $value->value()
+      if ref($value) && $value->isa('Frontier::RPC2::DataType');
+    $_=>$value;
+  } keys %$arg;
+}
+
+
diff --git a/FS/FS/part_export/ldap.pm b/FS/FS/part_export/ldap.pm
new file mode 100644 (file)
index 0000000..57fd1f3
--- /dev/null
@@ -0,0 +1,253 @@
+package FS::part_export::ldap;
+
+use vars qw(@ISA @saltset);
+use FS::Record qw( dbh );
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self, $svc_acct) = (shift, shift);
+
+  #false laziness w/shellcommands.pm
+  {
+    no strict 'refs';
+    ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
+    ${$_} = $svc_acct->$_() foreach qw( domain );
+    my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+    if ( $cust_pkg ) {
+      my $cust_main = $cust_pkg->cust_main;
+      ${$_} = $cust_main->getfield($_) foreach qw(first last);
+    }
+  }
+  $crypt_password = ''; #surpress "used only once" warnings
+  $crypt_password = '{crypt}'. crypt( $svc_acct->_password,
+                             $saltset[int(rand(64))].$saltset[int(rand(64))] );
+
+  my $username_attrib;
+  my %attrib = map    { /^\s*(\w+)\s+(.*\S)\s*$/;
+                        $username_attrib = $1 if $2 eq '$username';
+                        ( $1 => eval(qq("$2")) );                   }
+                 grep { /^\s*(\w+)\s+(.*\S)\s*$/ }
+                   split("\n", $self->option('attributes'));
+
+  if ( $self->option('radius') ) {
+    foreach my $table (qw(reply check)) {
+      my $method = "radius_$table";
+      my %radius = $svc_acct->$method();
+      foreach my $radius ( keys %radius ) {
+        ( my $ldap = $radius ) =~ s/\-//g;
+        $attrib{$ldap} = $radius{$radius};
+      }
+    }
+  }
+
+  my $err_or_queue = $self->ldap_queue( $svc_acct->svcnum, 'insert',
+    #$svc_acct->username,
+    $username_attrib,
+    %attrib );
+  return $err_or_queue unless ref($err_or_queue);
+
+  #groups with LDAP?
+  #my @groups = $svc_acct->radius_groups;
+  #if ( @groups ) {
+  #  my $err_or_queue = $self->ldap_queue(
+  #    $svc_acct->svcnum, 'usergroup_insert',
+  #    $svc_acct->username, @groups );
+  #  return $err_or_queue unless ref($err_or_queue);
+  #}
+
+  '';
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  return "can't (yet?) change username with ldap"
+    if $old->username ne $new->username;
+
+  return "ldap replace unimplemented";
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $jobnum = '';
+  #if ( $old->username ne $new->username ) {
+  #  my $err_or_queue = $self->ldap_queue( $new->svcnum, 'rename',
+  #    $new->username, $old->username );
+  #  unless ( ref($err_or_queue) ) {
+  #    $dbh->rollback if $oldAutoCommit;
+  #    return $err_or_queue;
+  #  }
+  #  $jobnum = $err_or_queue->jobnum;
+  #}
+
+  foreach my $table (qw(reply check)) {
+    my $method = "radius_$table";
+    my %new = $new->$method();
+    my %old = $old->$method();
+    if ( grep { !exists $old{$_} #new attributes
+                || $new{$_} ne $old{$_} #changed
+              } keys %new
+    ) {
+      my $err_or_queue = $self->ldap_queue( $new->svcnum, 'insert',
+        $table, $new->username, %new );
+      unless ( ref($err_or_queue) ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $err_or_queue;
+      }
+      if ( $jobnum ) {
+        my $error = $err_or_queue->depend_insert( $jobnum );
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return $error;
+        }
+      }
+    }
+
+    my @del = grep { !exists $new{$_} } keys %old;
+    if ( @del ) {
+      my $err_or_queue = $self->ldap_queue( $new->svcnum, 'attrib_delete',
+        $table, $new->username, @del );
+      unless ( ref($err_or_queue) ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $err_or_queue;
+      }
+      if ( $jobnum ) {
+        my $error = $err_or_queue->depend_insert( $jobnum );
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return $error;
+        }
+      }
+    }
+  }
+
+  # (sorta) false laziness with FS::svc_acct::replace
+  my @oldgroups = @{$old->usergroup}; #uuuh
+  my @newgroups = $new->radius_groups;
+  my @delgroups = ();
+  foreach my $oldgroup ( @oldgroups ) {
+    if ( grep { $oldgroup eq $_ } @newgroups ) {
+      @newgroups = grep { $oldgroup ne $_ } @newgroups;
+      next;
+    }
+    push @delgroups, $oldgroup;
+  }
+
+  if ( @delgroups ) {
+    my $err_or_queue = $self->ldap_queue( $new->svcnum, 'usergroup_delete',
+      $new->username, @delgroups );
+    unless ( ref($err_or_queue) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $err_or_queue;
+    }
+    if ( $jobnum ) {
+      my $error = $err_or_queue->depend_insert( $jobnum );
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+
+  if ( @newgroups ) {
+    my $err_or_queue = $self->ldap_queue( $new->svcnum, 'usergroup_insert',
+      $new->username, @newgroups );
+    unless ( ref($err_or_queue) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $err_or_queue;
+    }
+    if ( $jobnum ) {
+      my $error = $err_or_queue->depend_insert( $jobnum );
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+sub _export_delete {
+  my( $self, $svc_acct ) = (shift, shift);
+  return "ldap delete unimplemented";
+  my $err_or_queue = $self->ldap_queue( $svc_acct->svcnum, 'delete',
+    $svc_acct->username );
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub ldap_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::ldap::ldap_$method",
+  };
+  $queue->insert(
+    $self->machine,
+    $self->option('dn'),
+    $self->option('password'),
+    $self->option('userdn'),
+    @_,
+  ) or $queue;
+}
+
+sub ldap_insert { #subroutine, not method
+  my $ldap = ldap_connect(shift, shift, shift);
+  my( $userdn, $username_attrib, %attrib ) = @_;
+
+  $userdn = "$username_attrib=$attrib{$username_attrib}, $userdn"
+    if $username_attrib;
+  #icky hack, but should be unsurprising to the LDAPers
+  foreach my $key ( grep { $attrib{$_} =~ /,/ } keys %attrib ) {
+    $attrib{$key} = [ split(/,/, $attrib{$key}) ]; 
+  }
+
+  my $status = $ldap->add( $userdn, attrs => [ %attrib ] );
+  die 'LDAP error: '. $status->error. "\n" if $status->is_error;
+
+  $ldap->unbind;
+}
+
+#sub ldap_delete { #subroutine, not method
+#  my $dbh = ldap_connect(shift, shift, shift);
+#  my $username = shift;
+#
+#  foreach my $table (qw( radcheck radreply usergroup )) {
+#    my $sth = $dbh->prepare( "DELETE FROM $table WHERE UserName = ?" );
+#    $sth->execute($username)
+#      or die "can't delete from $table table: ". $sth->errstr;
+#  }
+#  $dbh->disconnect;
+#}
+
+sub ldap_connect {
+  my( $machine, $dn, $password ) = @_;
+  my %bind_options;
+  $bind_options{password} = $password if length($password);
+
+  eval "use Net::LDAP";
+  die $@ if $@;
+
+  my $ldap = Net::LDAP->new($machine) or die $@;
+  my $status = $ldap->bind( $dn, %bind_options );
+  die 'LDAP error: '. $status->error. "\n" if $status->is_error;
+
+  $ldap;
+}
+
diff --git a/FS/FS/part_export/null.pm b/FS/FS/part_export/null.pm
new file mode 100644 (file)
index 0000000..0145af3
--- /dev/null
@@ -0,0 +1,13 @@
+package FS::part_export::null;
+
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {}
+sub _export_replace {}
+sub _export_delete {}
+
diff --git a/FS/FS/part_export/shellcommands.pm b/FS/FS/part_export/shellcommands.pm
new file mode 100644 (file)
index 0000000..edc9440
--- /dev/null
@@ -0,0 +1,125 @@
+package FS::part_export::shellcommands;
+
+use vars qw(@ISA @saltset);
+use String::ShellQuote;
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self) = shift;
+  $self->_export_command('useradd', @_);
+}
+
+sub _export_delete {
+  my($self) = shift;
+  $self->_export_command('userdel', @_);
+}
+
+sub _export_suspend {
+  my($self) = shift;
+  $self->_export_command('suspend', @_);
+}
+
+sub _export_unsuspend {
+  my($self) = shift;
+  $self->_export_command('unsuspend', @_);
+}
+
+sub _export_command {
+  my ( $self, $action, $svc_acct) = (shift, shift, shift);
+  my $command = $self->option($action);
+  return '' if $command =~ /^\s*$/;
+  my $stdin = $self->option($action."_stdin");
+
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${$_} = $svc_acct->getfield($_) foreach $svc_acct->fields;
+  }
+
+  my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+  if ( $cust_pkg ) {
+    $email = ( grep { $_ ne 'POST' } $cust_pkg->cust_main->invoicing_list )[0];
+  } else {
+    $email = '';
+  }
+
+  $finger = shell_quote $finger;
+  $quoted_password = shell_quote $_password;
+  $domain = $svc_acct->domain;
+  $crypt_password = ''; #surpress "used only once" warnings
+  $crypt_password = crypt( $svc_acct->_password,
+                             $saltset[int(rand(64))].$saltset[int(rand(64))] );
+
+  $self->shellcommands_queue( $svc_acct->svcnum,
+    user         => $self->option('user')||'root',
+    host         => $self->machine,
+    command      => eval(qq("$command")),
+    stdin_string => eval(qq("$stdin")),
+  );
+}
+
+sub _export_replace {
+  my($self, $new, $old ) = (shift, shift, shift);
+  my $command = $self->option('usermod');
+  my $stdin = $self->option('usermod_stdin');
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${"old_$_"} = $old->getfield($_) foreach $old->fields;
+    ${"new_$_"} = $new->getfield($_) foreach $new->fields;
+  }
+  $new_finger = shell_quote $new_finger;
+  $quoted_new__password = shell_quote $new__password; #old, wrong?
+  $new_quoted_password = shell_quote $new__password; #new, better?
+  $old_domain = $old->domain;
+  $new_domain = $new->domain;
+  $new_crypt_password = ''; #surpress "used only once" warnings
+  $new_crypt_password = crypt( $new->_password,
+                               $saltset[int(rand(64))].$saltset[int(rand(64))]);
+  if ( $self->option('usermod_pwonly') ) {
+    my $error = '';
+    if ( $old_username ne $new_username ) {
+      $error ||= "can't change username";
+    }
+    if ( $old_domain ne $new_domain ) {
+      $error ||= "can't change domain";
+    }
+    return $error. ' ('. $self->exporttype. ' to '. $self->machine. ')'
+      if $error;
+  }
+  $self->shellcommands_queue( $new->svcnum,
+    user         => $self->option('user')||'root',
+    host         => $self->machine,
+    command      => eval(qq("$command")),
+    stdin_string => eval(qq("$stdin")),
+  );
+}
+
+#a good idea to queue anything that could fail or take any time
+sub shellcommands_queue {
+  my( $self, $svcnum ) = (shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::shellcommands::ssh_cmd",
+  };
+  $queue->insert( @_ );
+}
+
+sub ssh_cmd { #subroutine, not method
+  use Net::SSH '0.07';
+  &Net::SSH::ssh_cmd( { @_ } );
+}
+
+#sub shellcommands_insert { #subroutine, not method
+#}
+#sub shellcommands_replace { #subroutine, not method
+#}
+#sub shellcommands_delete { #subroutine, not method
+#}
+
diff --git a/FS/FS/part_export/shellcommands_withdomain.pm b/FS/FS/part_export/shellcommands_withdomain.pm
new file mode 100644 (file)
index 0000000..a15c24d
--- /dev/null
@@ -0,0 +1,7 @@
+package FS::part_export::shellcommands_withdomain;
+
+use vars qw(@ISA);
+use FS::part_export::shellcommands;
+
+@ISA = qw(FS::part_export::shellcommands);
+
diff --git a/FS/FS/part_export/sqlmail.pm b/FS/FS/part_export/sqlmail.pm
new file mode 100644 (file)
index 0000000..8ccad3c
--- /dev/null
@@ -0,0 +1,182 @@
+package FS::part_export::sqlmail;
+
+use vars qw(@ISA);
+use Digest::MD5 qw(md5_hex);
+use FS::Record qw(qsearchs);
+use FS::part_export;
+use FS::svc_domain;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self, $svc) = (shift, shift);
+  # this is a svc_something.
+
+  my $svcdb = $svc->cust_svc->part_svc->svcdb;
+  my $export_table = $self->option($svcdb . '_table')
+    or die('Export table not defined for svcdb: ' . $svcdb);
+  my @export_fields = split(/\s+/, $self->option($svcdb . '_fields'));
+  my $svchash = update_values($self, $svc, $svcdb);
+
+  foreach my $key (keys(%$svchash)) {
+    unless (grep { $key eq $_ } @export_fields) {
+      delete $svchash->{$key};
+    }
+  }
+
+  my $error = $self->sqlmail_queue( $svc->svcnum, 'insert',
+    $self->option('server_type'), $export_table,
+    (map { ($_, $svchash->{$_}); } keys(%$svchash)));
+  return $error if $error;
+  '';
+
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+
+  my $svcdb = $new->cust_svc->part_svc->svcdb;
+  my $export_table = $self->option($svcdb . '_table')
+    or die('Export table not defined for svcdb: ' . $svcdb);
+  my @export_fields = split(/\s+/, $self->option($svcdb . '_fields'));
+  my $svchash = update_values($self, $new, $svcdb);
+
+  foreach my $key (keys(%$svchash)) {
+    unless (grep { $key eq $_ } @export_fields) {
+      delete $svchash->{$key};
+    }
+  }
+
+  my $error = $self->sqlmail_queue( $new->svcnum, 'replace',
+    $old->svcnum, $self->option('server_type'), $export_table,
+    (map { ($_, $svchash->{$_}); } keys(%$svchash)));
+  return $error if $error;
+  '';
+
+}
+
+sub _export_delete {
+  my( $self, $svc ) = (shift, shift);
+
+  my $svcdb = $svc->cust_svc->part_svc->svcdb;
+  my $table = $self->option($svcdb . '_table')
+    or die('Export table not defined for svcdb: ' . $svcdb);
+
+  $self->sqlmail_queue( $svc->svcnum, 'delete', $table,
+    $svc->svcnum );
+}
+
+sub sqlmail_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::sqlmail::sqlmail_$method",
+  };
+  $queue->insert(
+    $self->option('datasrc'),
+    $self->option('username'),
+    $self->option('password'),
+    @_,
+  );
+}
+
+sub sqlmail_insert { #subroutine, not method
+  my $dbh = sqlmail_connect(shift, shift, shift);
+  my( $server_type, $table ) = (shift, shift);
+
+  my %attrs = @_;
+
+  map { $attrs{$_} = $attrs{$_} ? qq!'$attrs{$_}'! : 'NULL'; } keys(%attrs);
+  my $query = sprintf("INSERT INTO %s (%s) values (%s)",
+                      $table, join(",", keys(%attrs)),
+                      join(',', values(%attrs)));
+
+  $dbh->do($query) or die $dbh->errstr;
+  $dbh->disconnect;
+
+  '';
+}
+
+sub sqlmail_delete { #subroutine, not method
+  my $dbh = sqlmail_connect(shift, shift, shift);
+  my( $table, $svcnum ) = @_;
+
+  $dbh->do("DELETE FROM $table WHERE svcnum = $svcnum") or die $dbh->errstr;
+  $dbh->disconnect;
+
+  '';
+}
+
+sub sqlmail_replace {
+  my $dbh = sqlmail_connect(shift, shift, shift);
+  my($oldsvcnum, $server_type, $table) = (shift, shift, shift);
+
+  my %attrs = @_;
+  map { $attrs{$_} = $attrs{$_} ? qq!'$attrs{$_}'! : 'NULL'; } keys(%attrs);
+
+  my $query = "SELECT COUNT(*) FROM $table WHERE svcnum = $oldsvcnum";
+  my $result = $dbh->selectrow_arrayref($query) or die $dbh->errstr;
+  
+  if (@$result[0] == 0) {
+    $query = sprintf("INSERT INTO %s (%s) values (%s)",
+                     $table, join(",", keys(%attrs)),
+                     join(',', values(%attrs)));
+    $dbh->do($query) or die $dbh->errstr;
+  } else {
+    $query = sprintf('UPDATE %s SET %s WHERE svcnum = %s',
+                     $table, join(', ', map {"$_ = $attrs{$_}"} keys(%attrs)),
+                     $oldsvcnum);
+    $dbh->do($query) or die $dbh->errstr;
+  }
+
+  $dbh->disconnect;
+
+  '';
+}
+
+sub sqlmail_connect {
+  DBI->connect(@_) or die $DBI::errstr;
+}
+
+sub update_values {
+
+  # Update records to conform to a particular server_type.
+
+  my ($self, $svc, $svcdb) = (shift,shift,shift);
+  my $svchash = { %{$svc->hashref} } or return ''; # We need a copy.
+
+  if ($svcdb eq 'svc_acct') {
+    if ($self->option('server_type') eq 'courier_crypt') {
+      my $salt = join '', ('.', '/', 0..9,'A'..'Z', 'a'..'z')[rand 64, rand 64];
+      $svchash->{_password} = crypt($svchash->{_password}, $salt);
+
+    } elsif ($self->option('server_type') eq 'dovecot_plain') {
+      $svchash->{_password} = '{PLAIN}' . $svchash->{_password};
+      
+    } elsif ($self->option('server_type') eq 'dovecot_crypt') {
+      my $salt = join '', ('.', '/', 0..9,'A'..'Z', 'a'..'z')[rand 64, rand 64];
+      $svchash->{_password} = '{CRYPT}' . crypt($svchash->{_password}, $salt);
+
+    } elsif ($self->option('server_type') eq 'dovecot_digest_md5') {
+      my $svc_domain = qsearchs('svc_domain', { svcnum => $svc->domsvc });
+      die('Unable to lookup svc_domain with domsvc: ' . $svc->domsvc)
+        unless ($svc_domain);
+
+      my $domain = $svc_domain->domain;
+      my $md5hash = '{DIGEST-MD5}' . md5_hex(join(':', $svchash->{username},
+                                             $domain, $svchash->{_password}));
+      $svchash->{_password} = $md5hash;
+    }
+  } elsif ($svcdb eq 'svc_forward') {
+    if ($self->option('resolve_dstsvc') && $svc->dstsvc_acct) {
+      $svchash->{dst} = $svc->dstsvc_acct->username . '@' .
+                        $svc->dstsvc_acct->svc_domain->domain;
+    }
+  }
+
+  return($svchash);
+
+}
+
diff --git a/FS/FS/part_export/sqlradius.pm b/FS/FS/part_export/sqlradius.pm
new file mode 100644 (file)
index 0000000..8a8f9be
--- /dev/null
@@ -0,0 +1,282 @@
+package FS::part_export::sqlradius;
+
+use vars qw(@ISA);
+use FS::Record qw( dbh );
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub export_username {
+  my($self, $svc_acct) = (shift, shift);
+  $svc_acct->username;
+}
+
+sub _export_insert {
+  my($self, $svc_acct) = (shift, shift);
+
+  foreach my $table (qw(reply check)) {
+    my $method = "radius_$table";
+    my %attrib = $svc_acct->$method();
+    next unless keys %attrib;
+    my $err_or_queue = $self->sqlradius_queue( $svc_acct->svcnum, 'insert',
+      $table, $self->export_username($svc_acct), %attrib );
+    return $err_or_queue unless ref($err_or_queue);
+  }
+  my @groups = $svc_acct->radius_groups;
+  if ( @groups ) {
+    my $err_or_queue = $self->sqlradius_queue(
+      $svc_acct->svcnum, 'usergroup_insert',
+      $self->export_username($svc_acct), @groups );
+    return $err_or_queue unless ref($err_or_queue);
+  }
+  '';
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $jobnum = '';
+  if ( $self->export_username($old) ne $self->export_username($new) ) {
+    my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'rename',
+      $self->export_username($new), $self->export_username($old) );
+    unless ( ref($err_or_queue) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $err_or_queue;
+    }
+    $jobnum = $err_or_queue->jobnum;
+  }
+
+  foreach my $table (qw(reply check)) {
+    my $method = "radius_$table";
+    my %new = $new->$method();
+    my %old = $old->$method();
+    if ( grep { !exists $old{$_} #new attributes
+                || $new{$_} ne $old{$_} #changed
+              } keys %new
+    ) {
+      my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'insert',
+        $table, $self->export_username($new), %new );
+      unless ( ref($err_or_queue) ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $err_or_queue;
+      }
+      if ( $jobnum ) {
+        my $error = $err_or_queue->depend_insert( $jobnum );
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return $error;
+        }
+      }
+    }
+
+    my @del = grep { !exists $new{$_} } keys %old;
+    if ( @del ) {
+      my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'attrib_delete',
+        $table, $self->export_username($new), @del );
+      unless ( ref($err_or_queue) ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $err_or_queue;
+      }
+      if ( $jobnum ) {
+        my $error = $err_or_queue->depend_insert( $jobnum );
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return $error;
+        }
+      }
+    }
+  }
+
+  # (sorta) false laziness with FS::svc_acct::replace
+  my @oldgroups = @{$old->usergroup}; #uuuh
+  my @newgroups = $new->radius_groups;
+  my @delgroups = ();
+  foreach my $oldgroup ( @oldgroups ) {
+    if ( grep { $oldgroup eq $_ } @newgroups ) {
+      @newgroups = grep { $oldgroup ne $_ } @newgroups;
+      next;
+    }
+    push @delgroups, $oldgroup;
+  }
+
+  if ( @delgroups ) {
+    my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'usergroup_delete',
+      $self->export_username($new), @delgroups );
+    unless ( ref($err_or_queue) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $err_or_queue;
+    }
+    if ( $jobnum ) {
+      my $error = $err_or_queue->depend_insert( $jobnum );
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+
+  if ( @newgroups ) {
+    my $err_or_queue = $self->sqlradius_queue( $new->svcnum, 'usergroup_insert',
+      $self->export_username($new), @newgroups );
+    unless ( ref($err_or_queue) ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $err_or_queue;
+    }
+    if ( $jobnum ) {
+      my $error = $err_or_queue->depend_insert( $jobnum );
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+sub _export_delete {
+  my( $self, $svc_acct ) = (shift, shift);
+  my $err_or_queue = $self->sqlradius_queue( $svc_acct->svcnum, 'delete',
+    $self->export_username($svc_acct) );
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub sqlradius_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::sqlradius::sqlradius_$method",
+  };
+  $queue->insert(
+    $self->option('datasrc'),
+    $self->option('username'),
+    $self->option('password'),
+    @_,
+  ) or $queue;
+}
+
+sub sqlradius_insert { #subroutine, not method
+  my $dbh = sqlradius_connect(shift, shift, shift);
+  my( $table, $username, %attributes ) = @_;
+
+  foreach my $attribute ( keys %attributes ) {
+  
+    my $s_sth = $dbh->prepare(
+      "SELECT COUNT(*) FROM rad$table WHERE UserName = ? AND Attribute = ?"
+    ) or die $dbh->errstr;
+    $s_sth->execute( $username, $attribute ) or die $s_sth->errstr;
+
+    if ( $s_sth->fetchrow_arrayref->[0] ) {
+
+      my $u_sth = $dbh->prepare(
+        "UPDATE rad$table SET Value = ? WHERE UserName = ? AND Attribute = ?"
+      ) or die $dbh->errstr;
+      $u_sth->execute($attributes{$attribute}, $username, $attribute)
+        or die $u_sth->errstr;
+
+    } else {
+
+      my $i_sth = $dbh->prepare(
+        "INSERT INTO rad$table ( UserName, Attribute, op, Value ) ".
+          "VALUES ( ?, ?, ?, ? )"
+      ) or die $dbh->errstr;
+      $i_sth->execute(
+        $username,
+        $attribute,
+        ( $attribute =~ /Password/i ? '==' : ':=' ),
+        $attributes{$attribute},
+      ) or die $i_sth->errstr;
+
+    }
+
+  }
+  $dbh->disconnect;
+}
+
+sub sqlradius_usergroup_insert { #subroutine, not method
+  my $dbh = sqlradius_connect(shift, shift, shift);
+  my( $username, @groups ) = @_;
+
+  my $sth = $dbh->prepare( 
+    "INSERT INTO usergroup ( UserName, GroupName ) VALUES ( ?, ? )"
+  ) or die $dbh->errstr;
+  foreach my $group ( @groups ) {
+    $sth->execute( $username, $group )
+      or die "can't insert into groupname table: ". $sth->errstr;
+  }
+  $dbh->disconnect;
+}
+
+sub sqlradius_usergroup_delete { #subroutine, not method
+  my $dbh = sqlradius_connect(shift, shift, shift);
+  my( $username, @groups ) = @_;
+
+  my $sth = $dbh->prepare( 
+    "DELETE FROM usergroup WHERE UserName = ? AND GroupName = ?"
+  ) or die $dbh->errstr;
+  foreach my $group ( @groups ) {
+    $sth->execute( $username, $group )
+      or die "can't delete from groupname table: ". $sth->errstr;
+  }
+  $dbh->disconnect;
+}
+
+sub sqlradius_rename { #subroutine, not method
+  my $dbh = sqlradius_connect(shift, shift, shift);
+  my($new_username, $old_username) = @_;
+  foreach my $table (qw(radreply radcheck usergroup )) {
+    my $sth = $dbh->prepare("UPDATE $table SET Username = ? WHERE UserName = ?")
+      or die $dbh->errstr;
+    $sth->execute($new_username, $old_username)
+      or die "can't update $table: ". $sth->errstr;
+  }
+  $dbh->disconnect;
+}
+
+sub sqlradius_attrib_delete { #subroutine, not method
+  my $dbh = sqlradius_connect(shift, shift, shift);
+  my( $table, $username, @attrib ) = @_;
+
+  foreach my $attribute ( @attrib ) {
+    my $sth = $dbh->prepare(
+        "DELETE FROM rad$table WHERE UserName = ? AND Attribute = ?" )
+      or die $dbh->errstr;
+    $sth->execute($username,$attribute)
+      or die "can't delete from rad$table table: ". $sth->errstr;
+  }
+  $dbh->disconnect;
+}
+
+sub sqlradius_delete { #subroutine, not method
+  my $dbh = sqlradius_connect(shift, shift, shift);
+  my $username = shift;
+
+  foreach my $table (qw( radcheck radreply usergroup )) {
+    my $sth = $dbh->prepare( "DELETE FROM $table WHERE UserName = ?" );
+    $sth->execute($username)
+      or die "can't delete from $table table: ". $sth->errstr;
+  }
+  $dbh->disconnect;
+}
+
+sub sqlradius_connect {
+  #my($datasrc, $username, $password) = @_;
+  #DBI->connect($datasrc, $username, $password) or die $DBI::errstr;
+  DBI->connect(@_) or die $DBI::errstr;
+}
+
diff --git a/FS/FS/part_export/sqlradius_withdomain.pm b/FS/FS/part_export/sqlradius_withdomain.pm
new file mode 100644 (file)
index 0000000..1c8f38c
--- /dev/null
@@ -0,0 +1,12 @@
+package FS::part_export::sqlradius_withdomain;
+
+use vars qw(@ISA);
+use FS::part_export::sqlradius;
+
+@ISA = qw(FS::part_export::sqlradius);
+
+sub export_username {
+  my($self, $svc_acct) = (shift, shift);
+  $svc_acct->email;
+}
+
diff --git a/FS/FS/part_export/sysvshell.pm b/FS/FS/part_export/sysvshell.pm
new file mode 100644 (file)
index 0000000..f3f6b34
--- /dev/null
@@ -0,0 +1,7 @@
+package FS::part_export::sysvshell;
+
+use vars qw(@ISA);
+use FS::part_export::null;
+
+@ISA = qw(FS::part_export::null);
+
diff --git a/FS/FS/part_export/textradius.pm b/FS/FS/part_export/textradius.pm
new file mode 100644 (file)
index 0000000..1492f26
--- /dev/null
@@ -0,0 +1,166 @@
+package FS::part_export::textradius;
+
+use vars qw(@ISA $prefix);
+use Fcntl qw(:flock);
+use FS::UID qw(datasrc);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+$prefix = "/usr/local/etc/freeside/export.";
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self, $svc_acct) = (shift, shift);
+  $err_or_queue = $self->textradius_queue( $svc_acct->svcnum, 'insert',
+    $svc_acct->username, $svc_acct->radius_check, '-', $svc_acct->radius_reply);
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+  return "can't (yet?) change username with textradius"
+    if $old->username ne $new->username;
+  #return '' unless $old->_password ne $new->_password;
+  $err_or_queue = $self->textradius_queue( $new->svcnum, 'insert',
+    $new->username, $new->radius_check, '-', $new->radius_reply);
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub _export_delete {
+  my( $self, $svc_acct ) = (shift, shift);
+  $err_or_queue = $self->textradius_queue( $svc_acct->svcnum, 'delete',
+    $svc_acct->username );
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+#a good idea to queue anything that could fail or take any time
+sub textradius_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::textradius::textradius_$method",
+  };
+  $queue->insert(
+    $self->option('user')||'root',
+    $self->machine,
+    $self->option('users'),
+    @_,
+  ) or $queue;
+}
+
+sub textradius_insert { #subroutine, not method
+  my( $user, $host, $users, $username, @attributes ) = @_;
+
+  #silly arg processing
+  my($att, @check);
+  push @check, $att while @attributes && ($att=shift @attributes) ne '-';
+  my %check = @check;
+  my %reply = @attributes;
+
+  my $file = textradius_download($user, $host, $users);
+
+  eval "use RADIUS::UserFile;";
+  die $@ if $@;
+
+  my $userfile = new RADIUS::UserFile(
+    File        => $file,
+    Who         => [ $username ],
+    Check_Items => [ keys %check ],
+  ) or die "error parsing $file";
+
+  $userfile->remove($username);
+  $userfile->add(
+    Who        => $username,
+    Attributes => { %check, %reply },
+    Comment    => 'user added by Freeside',
+  ) or die "error adding to $file";
+
+  $userfile->update( Who => [ $username ] )
+    or die "error updating $file";
+
+  textradius_upload($user, $host, $users);
+
+}
+
+sub textradius_delete { #subroutine, not method
+  my( $user, $host, $users, $username ) = @_;
+
+  my $file = textradius_download($user, $host, $users);
+
+  eval "use RADIUS::UserFile;";
+  die $@ if $@;
+
+  my $userfile = new RADIUS::UserFile(
+    File        => $file,
+    Who         => [ $username ],
+  ) or die "error parsing $file";
+
+  $userfile->remove($username);
+
+  $userfile->update( Who => [ $username ] )
+    or die "error updating $file";
+
+  textradius_upload($user, $host, $users);
+}
+
+sub textradius_download {
+  my( $user, $host, $users ) = @_;
+
+  my $dir = $prefix. datasrc;
+  mkdir $dir, 0700 or die $! unless -d $dir;
+  $dir .= "/$host";
+  mkdir $dir, 0700 or die $! unless -d $dir;
+
+  my $dest = "$dir/users";
+
+  eval "use File::Rsync;";
+  die $@ if $@;
+  my $rsync = File::Rsync->new({ rsh => 'ssh' });
+
+  open(LOCK, "+>>$dest.lock")
+    and flock(LOCK,LOCK_EX)
+      or die "can't open $dest.lock: $!";
+
+  $rsync->exec( {
+    src  => "$user\@$host:$users",
+    dest => $dest,
+  } ); # true/false return value from exec is not working, alas
+  if ( $rsync->err ) {
+    die "error downloading $user\@$host:$users : ".
+        'exit status: '. $rsync->status. ', '.
+        'STDERR: '. join(" / ", $rsync->err). ', '.
+        'STDOUT: '. join(" / ", $rsync->out);
+  }
+
+  $dest;
+}
+
+sub textradius_upload {
+  my( $user, $host, $users ) = @_;
+
+  my $dir = $prefix. datasrc. "/$host";
+
+  eval "use File::Rsync;";
+  die $@ if $@;
+  my $rsync = File::Rsync->new({
+    rsh => 'ssh',
+    #dry_run => 1,
+  });
+  $rsync->exec( {
+    src  => "$dir/users",
+    dest => "$user\@$host:$users",
+  } ); # true/false return value from exec is not working, alas
+  if ( $rsync->err ) {
+    die "error uploading to $user\@$host:$users : ".
+        'exit status: '. $rsync->status. ', '.
+        'STDERR: '. join(" / ", $rsync->err). ', '.
+        'STDOUT: '. join(" / ", $rsync->out);
+  }
+
+  flock(LOCK,LOCK_UN);
+  close LOCK;
+
+}
+
diff --git a/FS/FS/part_export/vpopmail.pm b/FS/FS/part_export/vpopmail.pm
new file mode 100644 (file)
index 0000000..a505a0f
--- /dev/null
@@ -0,0 +1,226 @@
+package FS::part_export::vpopmail;
+
+use vars qw(@ISA @saltset $exportdir);
+use Fcntl qw(:flock);
+use File::Path;
+use FS::UID qw( datasrc );
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self, $svc_acct) = (shift, shift);
+  $self->vpopmail_queue( $svc_acct->svcnum, 'insert',
+    $svc_acct->username,
+    crypt($svc_acct->_password,$saltset[int(rand(64))].$saltset[int(rand(64))]),
+    $svc_acct->domain,
+    $svc_acct->quota,
+    $svc_acct->finger,
+  );
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+
+  my $cpassword = crypt(
+    $new->_password, $saltset[int(rand(64))].$saltset[int(rand(64))]
+  );
+
+  return "can't change username with vpopmail"
+    if $old->username ne $new->username;
+
+  #no.... if mail can't be preserved, better to disallow username changes
+  #if ($old->username ne $new->username || $old->domain ne $new->domain ) {
+  #  vpopmail_queue( $svc_acct->svcnum, 'delete', 
+  #    $old->username, $old->domain
+  #  );
+  #  vpopmail_queue( $svc_acct->svcnum, 'insert', 
+  #    $new->username,
+  #    $cpassword,
+  #    $new->domain,
+  #  );
+
+  return '' unless $old->_password ne $new->_password;
+
+  $self->vpopmail_queue( $new->svcnum, 'replace',
+    $new->username, $cpassword, $new->domain, $new->quota, $new->finger );
+}
+
+sub _export_delete {
+  my( $self, $svc_acct ) = (shift, shift);
+  $self->vpopmail_queue( $svc_acct->svcnum, 'delete',
+    $svc_acct->username, $svc_acct->domain );
+}
+
+#a good idea to queue anything that could fail or take any time
+sub vpopmail_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+
+  my $exportdir = "/usr/local/etc/freeside/export." . datasrc;
+  mkdir $exportdir, 0700 or die $! unless -d $exportdir;
+  $exportdir .= "/vpopmail";
+  mkdir $exportdir, 0700 or die $! unless -d $exportdir;
+  $exportdir .= '/'. $self->machine;
+  mkdir $exportdir, 0700 or die $! unless -d $exportdir;
+  mkdir "$exportdir/domains", 0700 or die $! unless -d "$exportdir/domains";
+
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::vpopmail::vpopmail_$method",
+  };
+  $queue->insert(
+    $exportdir,
+    $self->machine,
+    $self->option('dir'),
+    $self->option('uid'),
+    $self->option('gid'),
+    $self->option('restart'),
+    @_
+  );
+}
+
+sub vpopmail_insert { #subroutine, not method
+  my( $exportdir, $machine, $dir, $uid, $gid, $restart ) = splice @_,0,6;
+  my( $username, $password, $domain, $quota, $finger ) = @_;
+
+  mkdir "$exportdir/domains/$domain", 0700 or die $!
+    unless -d "$exportdir/domains/$domain";
+
+  (open(VPASSWD, ">>$exportdir/domains/$domain/vpasswd")
+    and flock(VPASSWD,LOCK_EX)
+  ) or die "can't open vpasswd file for $username\@$domain: ".
+           "$exportdir/domains/$domain/vpasswd: $!";
+  print VPASSWD join(":",
+    $username,
+    $password,
+    '1',
+    '0',
+    $finger,
+    "$dir/domains/$domain/$username",
+    $quota ? $quota.'S' : 'NOQUOTA',
+  ), "\n";
+
+  flock(VPASSWD,LOCK_UN);
+  close(VPASSWD);
+
+  for my $mkdir (
+    grep { ! -d $_ } map { "$exportdir/domains/$domain/$username$_" }
+        ( '', qw( /Maildir /Maildir/cur /Maildir/new /Maildir/tmp ) )
+  ) {
+    mkdir $mkdir, 0700 or die "can't mkdir $mkdir: $!";
+  }
+
+  vpopmail_sync( $exportdir, $machine, $dir, $uid, $gid, $restart );
+
+}
+
+sub vpopmail_replace { #subroutine, not method
+  my( $exportdir, $machine, $dir, $uid, $gid, $restart ) = splice @_,0,6;
+  my( $username, $password, $domain, $quota, $finger ) = @_;
+  
+  (open(VPASSWD, "$exportdir/domains/$domain/vpasswd")
+    and flock(VPASSWD,LOCK_EX)
+  ) or die "can't open $exportdir/domains/$domain/vpasswd: $!";
+
+  open(VPASSWDTMP, ">$exportdir/domains/$domain/vpasswd.tmp")
+    or die "Can't open $exportdir/domains/$domain/vpasswd.tmp: $!";
+
+  while (<VPASSWD>) {
+    my ($mailbox, $pw, $vuid, $vgid, $vfinger, $vdir, $vquota, @rest) =
+      split(':', $_);
+    if ( $username ne $mailbox ) {
+      print VPASSWDTMP $_;
+      next
+    }
+    print VPASSWDTMP join (':',
+      $mailbox,
+      $password,
+      '1',
+      '0',
+      $finger,
+      "$dir/domains/$domain/$username", #$vdir
+      $quota ? $quota.'S' : 'NOQUOTA',
+    ), "\n";
+  }
+
+  close(VPASSWDTMP);
+
+  rename "$exportdir/domains/$domain/vpasswd.tmp", "$exportdir/domains/$domain/vpasswd"
+    or die "Can't rename $exportdir/domains/$domain/vpasswd.tmp: $!";
+
+  flock(VPASSWD,LOCK_UN);
+  close(VPASSWD);
+
+  vpopmail_sync( $exportdir, $machine, $dir, $uid, $gid, $restart );
+
+}
+
+sub vpopmail_delete { #subroutine, not method
+  my( $exportdir, $machine, $dir, $uid, $gid, $restart ) = splice @_,0,6;
+  my( $username, $domain ) = @_;
+  
+  (open(VPASSWD, "$exportdir/domains/$domain/vpasswd")
+    and flock(VPASSWD,LOCK_EX)
+  ) or die "can't open $exportdir/domains/$domain/vpasswd: $!";
+
+  open(VPASSWDTMP, ">$exportdir/domains/$domain/vpasswd.tmp")
+    or die "Can't open $exportdir/domains/$domain/vpasswd.tmp: $!";
+
+  while (<VPASSWD>) {
+    my ($mailbox, $rest) = split(':', $_);
+    print VPASSWDTMP $_ unless $username eq $mailbox;
+  }
+
+  close(VPASSWDTMP);
+
+  rename "$exportdir/domains/$domain/vpasswd.tmp",
+         "$exportdir/domains/$domain/vpasswd"
+    or die "Can't rename $exportdir/domains/$domain/vpasswd.tmp: $!";
+
+  flock(VPASSWD,LOCK_UN);
+  close(VPASSWD);
+
+  rmtree "$exportdir/domains/$domain/$username"
+    or die "can't rmtree $exportdir/domains/$domain/$username: $!";
+
+  vpopmail_sync( $exportdir, $machine, $dir, $uid, $gid, $restart );
+}
+
+sub vpopmail_sync {
+  my( $exportdir, $machine, $dir, $uid, $gid, $restart ) = splice @_,0,6;
+  
+  chdir $exportdir;
+#  my @args = ( $rsync, "-rlpt", "-e", $ssh, "domains/",
+#               "vpopmail\@$machine:$dir/domains/"  );
+#  system {$args[0]} @args;
+
+  eval "use File::Rsync;";
+  die $@ if $@;
+
+  my $rsync = File::Rsync->new({ rsh => 'ssh' });
+
+  $rsync->exec( {
+    recursive => 1,
+    perms     => 1,
+    times     => 1,
+    src       => "$exportdir/domains/",
+    dest      => "vpopmail\@$machine:$dir/domains/",
+  } ); # true/false return value from exec is not working, alas
+  if ( $rsync->err ) {
+    die "error uploading to vpopmail\@$machine:$dir/domains/ : ".
+        'exit status: '. $rsync->status. ', '.
+        'STDERR: '. join(" / ", $rsync->err). ', '.
+        'STDOUT: '. join(" / ", $rsync->out);
+  }
+
+  eval "use Net::SSH qw(ssh);";
+  die $@ if $@;
+
+  ssh("vpopmail\@$machine", $restart) if $restart;
+}
+
+
diff --git a/FS/FS/part_export/www_shellcommands.pm b/FS/FS/part_export/www_shellcommands.pm
new file mode 100644 (file)
index 0000000..20658c7
--- /dev/null
@@ -0,0 +1,109 @@
+package FS::part_export::www_shellcommands;
+
+use strict;
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self) = shift;
+  $self->_export_command('useradd', @_);
+}
+
+sub _export_delete {
+  my($self) = shift;
+  $self->_export_command('userdel', @_);
+}
+
+sub _export_command {
+  my ( $self, $action, $svc_www) = (shift, shift, shift);
+  my $command = $self->option($action);
+
+  #set variable for the command
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${$_} = $svc_www->getfield($_) foreach $svc_www->fields;
+  }
+  my $domain_record = $svc_www->domain_record; # or die ?
+  my $zone = $domain_record->zone; # or die ?
+  my $svc_acct = $svc_www->svc_acct; # or die ?
+  my $username = $svc_acct->username;
+  my $homedir = $svc_acct->dir; # or die ?
+
+  #done setting variables for the command
+
+  $self->shellcommands_queue( $svc_www->svcnum,
+    user         => $self->option('user')||'root',
+    host         => $self->machine,
+    command      => eval(qq("$command")),
+  );
+}
+
+sub _export_replace {
+  my($self, $new, $old ) = (shift, shift, shift);
+  my $command = $self->option('usermod');
+  
+  #set variable for the command
+  no strict 'vars';
+  {
+    no strict 'refs';
+    ${"old_$_"} = $old->getfield($_) foreach $old->fields;
+    ${"new_$_"} = $new->getfield($_) foreach $new->fields;
+  }
+  my $old_domain_record = $old->domain_record; # or die ?
+  my $old_zone = $old_domain_record->reczone; # or die ?
+  unless ( $old_zone =~ /\.$/ ) {
+    my $old_svc_domain = $old_domain_record->svc_domain; # or die ?
+    $old_zone .= '.'. $old_svc_domain->domain;
+  }
+
+  my $old_svc_acct = $old->svc_acct; # or die ?
+  my $old_username = $old_svc_acct->username;
+  my $old_homedir = $old_svc_acct->dir; # or die ?
+
+  my $new_domain_record = $new->domain_record; # or die ?
+  my $new_zone = $new_domain_record->reczone; # or die ?
+  unless ( $new_zone =~ /\.$/ ) {
+    my $new_svc_domain = $new_domain_record->svc_domain; # or die ?
+    $new_zone .= '.'. $new_svc_domain->domain;
+  }
+
+  my $new_svc_acct = $new->svc_acct; # or die ?
+  my $new_username = $new_svc_acct->username;
+  my $new_homedir = $new_svc_acct->dir; # or die ?
+
+  #done setting variables for the command
+
+  $self->shellcommands_queue( $new->svcnum,
+    user         => $self->option('user')||'root',
+    host         => $self->machine,
+    command      => eval(qq("$command")),
+  );
+}
+
+#a good idea to queue anything that could fail or take any time
+sub shellcommands_queue {
+  my( $self, $svcnum ) = (shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::www_shellcommands::ssh_cmd",
+  };
+  $queue->insert( @_ );
+}
+
+sub ssh_cmd { #subroutine, not method
+  use Net::SSH '0.07';
+  &Net::SSH::ssh_cmd( { @_ } );
+}
+
+#sub shellcommands_insert { #subroutine, not method
+#}
+#sub shellcommands_replace { #subroutine, not method
+#}
+#sub shellcommands_delete { #subroutine, not method
+#}
+
diff --git a/FS/FS/part_export_option.pm b/FS/FS/part_export_option.pm
new file mode 100644 (file)
index 0000000..a0b19fd
--- /dev/null
@@ -0,0 +1,134 @@
+package FS::part_export_option;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+use FS::part_export;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::part_export_option - Object methods for part_export_option records
+
+=head1 SYNOPSIS
+
+  use FS::part_export_option;
+
+  $record = new FS::part_export_option \%hash;
+  $record = new FS::part_export_option { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_export_option object represents an export option.
+FS::part_export_option inherits from FS::Record.  The following fields are
+currently supported:
+
+=over 4
+
+=item optionnum - primary key
+
+=item exportnum - export (see L<FS::part_export>)
+
+=item optionname - option name
+
+=item optionvalue - option value
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new export option.  To add the export option to the database, see
+L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_export_option'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid export option.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('optionnum')
+    || $self->ut_number('exportnum')
+    || $self->ut_alpha('optionname')
+    || $self->ut_anything('optionvalue')
+  ;
+  return $error if $error;
+
+  return "Unknown exportnum: ". $self->exportnum
+    unless qsearchs('part_export', { 'exportnum' => $self->exportnum } );
+
+  #check options & values?
+
+  ''; #no error
+}
+
+=back
+
+=head1 BUGS
+
+Possibly.
+
+=head1 SEE ALSO
+
+L<FS::part_export>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
new file mode 100644 (file)
index 0000000..6525864
--- /dev/null
@@ -0,0 +1,320 @@
+package FS::part_pkg;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch dbh );
+use FS::pkg_svc;
+use FS::agent_type;
+use FS::type_pkgs;
+use FS::Conf;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_pkg - Object methods for part_pkg objects
+
+=head1 SYNOPSIS
+
+  use FS::part_pkg;
+
+  $record = new FS::part_pkg \%hash
+  $record = new FS::part_pkg { 'column' => 'value' };
+
+  $custom_record = $template_record->clone;
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  @pkg_svc = $record->pkg_svc;
+
+  $svcnum = $record->svcpart;
+  $svcnum = $record->svcpart( 'svc_acct' );
+
+=head1 DESCRIPTION
+
+An FS::part_pkg object represents a billing item definition.  FS::part_pkg
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item pkgpart - primary key (assigned automatically for new billing item definitions)
+
+=item pkg - Text name of this billing item definition (customer-viewable)
+
+=item comment - Text name of this billing item definition (non-customer-viewable)
+
+=item setup - Setup fee expression
+
+=item freq - Frequency of recurring fee
+
+=item recur - Recurring fee expression
+
+=item setuptax - Setup fee tax exempt flag, empty or `Y'
+
+=item recurtax - Recurring fee tax exempt flag, empty or `Y'
+
+=item taxclass - Tax class flag
+
+=item plan - Price plan
+
+=item plandata - Price plan data
+
+=item disabled - Disabled flag, empty or `Y'
+
+=back
+
+setup and recur are evaluated as Safe perl expressions.  You can use numbers
+just as you would normally.  More advanced semantics are not yet defined.
+
+=head1 METHODS
+
+=over 4 
+
+=item new HASHREF
+
+Creates a new billing item definition.  To add the billing item definition to
+the database, see L<"insert">.
+
+=cut
+
+sub table { 'part_pkg'; }
+
+=item clone
+
+An alternate constructor.  Creates a new billing item definition by duplicating
+an existing definition.  A new pkgpart is assigned and `(CUSTOM) ' is prepended
+to the comment field.  To add the billing item definition to the database, see
+L<"insert">.
+
+=cut
+
+sub clone {
+  my $self = shift;
+  my $class = ref($self);
+  my %hash = $self->hash;
+  $hash{'pkgpart'} = '';
+  $hash{'comment'} = "(CUSTOM) ". $hash{'comment'}
+    unless $hash{'comment'} =~ /^\(CUSTOM\) /;
+  #new FS::part_pkg ( \%hash ); # ?
+  new $class ( \%hash ); # ?
+}
+
+=item insert
+
+Adds this billing item definition to the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  my $conf = new FS::Conf;
+
+  if ( $conf->exists('agent_defaultpkg') ) {
+    foreach my $agent_type ( qsearch('agent_type', {} ) ) {
+      my $type_pkgs = new FS::type_pkgs({
+        'typenum' => $agent_type->typenum,
+        'pkgpart' => $self->pkgpart,
+      });
+      my $error = $type_pkgs->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item delete
+
+Currently unimplemented.
+
+=cut
+
+sub delete {
+  return "Can't (yet?) delete package definitions.";
+# check & make sure the pkgpart isn't in cust_pkg or type_pkgs?
+}
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid billing item definition.  If
+there is an error, returns the error, otherwise returns false.  Called by the
+insert and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  for (qw(setup recur)) { $self->set($_=>0) if $self->get($_) =~ /^\s*$/; }
+
+  my $conf = new FS::Conf;
+  if ( $conf->exists('safe-part_pkg') ) {
+
+    my $error = $self->ut_anything('setup')
+                || $self->ut_anything('recur');
+    return $error if $error;
+
+    my $s = $self->setup;
+
+    $s =~ /^\s*\d*\.?\d*\s*$/
+
+      or $s =~ /^my \$d = \$cust_pkg->bill || \$time; \$d += 86400 \* \s*\d+\s*; \$cust_pkg->bill\(\$d\); \$cust_pkg_mod_flag=1; \s*\d*\.?\d*\s*$/
+
+      or do {
+        #log!
+        return "illegal setup: $s";
+      };
+
+    my $r = $self->recur;
+
+    $r =~ /^\s*\d*\.?\d*\s*$/
+
+      #or $r =~ /^\$sdate += 86400 \* \s*\d+\s*; \s*\d*\.?\d*\s*$/
+
+      or $r =~ /^my \$mnow = \$sdate; my \(\$sec,\$min,\$hour,\$mday,\$mon,\$year\) = \(localtime\(\$sdate\) \)\[0,1,2,3,4,5\]; my \$mstart = timelocal\(0,0,0,1,\$mon,\$year\); my \$mend = timelocal\(0,0,0,1, \$mon == 11 \? 0 : \$mon\+1, \$year\+\(\$mon==11\)\); \$sdate = \$mstart; \( \$part_pkg->freq \- 1 \) \* \d*\.?\d* \/ \$part_pkg\-\>freq \+ \d*\.?\d* \/ \$part_pkg\-\>freq \* \(\$mend\-\$mnow\) \/ \(\$mend\-\$mstart\) ;\s*$/
+
+      or $r =~ /^my \$mnow = \$sdate; my \(\$sec,\$min,\$hour,\$mday,\$mon,\$year\) = \(localtime\(\$sdate\) \)\[0,1,2,3,4,5\]; \$sdate = timelocal\(0,0,0,1,\$mon,\$year\); \s*\d*\.?\d*\s*;\s*$/
+
+      or $r =~ /^my \$error = \$cust_pkg\->cust_main\->credit\( \s*\d*\.?\d*\s* \* scalar\(\$cust_pkg\->cust_main\->referral_cust_main_ncancelled\(\s*\d+\s*\)\), "commission" \); die \$error if \$error; \s*\d*\.?\d*\s*;\s*$/
+
+      or $r =~ /^my \$error = \$cust_pkg\->cust_main\->credit\( \s*\d*\.?\d*\s* \* scalar\(\$cust_pkg\->cust_main->referral_cust_pkg\(\s*\d+\s*\)\), "commission" \); die \$error if \$error; \s*\d*\.?\d*\s*;\s*$/
+
+      or $r =~ /^my \$error = \$cust_pkg\->cust_main\->credit\( \s*\d*\.?\d*\s* \* scalar\( grep \{ my \$pkgpart = \$_\->pkgpart; grep \{ \$_ == \$pkgpart \} \(\s*(\s*\d+,\s*)*\s*\) \} \$cust_pkg\->cust_main->referral_cust_pkg\(\s*\d+\s*\)\), "commission" \); die \$error if \$error; \s*\d*\.?\d*\s*;\s*$/
+
+      or $r =~ /^my \$hours = \$cust_pkg\->seconds_since\(\$cust_pkg\->bill \|\| 0\) \/ 3600 \- \s*\d*\.?\d*\s*; \$hours = 0 if \$hours < 0; \s*\d*\.?\d*\s* \+ \s*\d*\.?\d*\s* \* \$hours;\s*$/
+
+      or $r =~ /^my \$min = \$cust_pkg\->seconds_since\(\$cust_pkg\->bill \|\| 0\) \/ 60 \- \s*\d*\.?\d*\s*; \$min = 0 if \$min < 0; \s*\d*\.?\d*\s* \+ \s*\d*\.?\d*\s* \* \$min;\s*$/
+
+      or $r =~ /^my \$last_bill = \$cust_pkg\->last_bill; my \$hours = \$cust_pkg\->seconds_since_sqlradacct\(\$last_bill, \$sdate \) \/ 3600 - \s*\d\.?\d*\s*; \$hours = 0 if \$hours < 0; my \$input = \$cust_pkg\->attribute_since_sqlradacct\(\$last_bill, \$sdate, "AcctInputOctets" \) \/ 1048576; my \$output = \$cust_pkg\->attribute_since_sqlradacct\(\$last_bill, \$sdate, "AcctOutputOctets" \) \/ 1048576; my \$total = \$input \+ \$output \- \s*\d\.?\d*\s*; \$total = 0 if \$total < 0; my \$input = \$input - \s*\d\.?\d*\s*; \$input = 0 if \$input < 0; my \$output = \$output - \s*\d\.?\d*\s*; \$output = 0 if \$output < 0; \s*\d\.?\d*\s* \+ \s*\d\.?\d*\s* \* \$hours \+ \s*\d\.?\d*\s* \* \$input \+ \s*\d\.?\d*\s* \* \$output \+ \s*\d\.?\d*\s* \* \$total *;\s*$/
+
+      or do {
+        #log!
+        return "illegal recur: $r";
+      };
+
+  }
+
+    $self->ut_numbern('pkgpart')
+      || $self->ut_text('pkg')
+      || $self->ut_text('comment')
+      || $self->ut_anything('setup')
+      || $self->ut_number('freq')
+      || $self->ut_anything('recur')
+      || $self->ut_alphan('plan')
+      || $self->ut_anything('plandata')
+      || $self->ut_enum('setuptax', [ '', 'Y' ] )
+      || $self->ut_enum('recurtax', [ '', 'Y' ] )
+      || $self->ut_textn('taxclass')
+      || $self->ut_enum('disabled', [ '', 'Y' ] )
+    ;
+}
+
+=item pkg_svc
+
+Returns all FS::pkg_svc objects (see L<FS::pkg_svc>) for this package
+definition (with non-zero quantity).
+
+=cut
+
+sub pkg_svc {
+  my $self = shift;
+  grep { $_->quantity } qsearch( 'pkg_svc', { 'pkgpart' => $self->pkgpart } );
+}
+
+=item svcpart [ SVCDB ]
+
+Returns the svcpart of a single service definition (see L<FS::part_svc>)
+associated with this billing item definition (see L<FS::pkg_svc>).  Returns
+false if there not exactly one service definition with quantity 1, or if 
+SVCDB is specified and does not match the svcdb of the service definition, 
+
+=cut
+
+sub svcpart {
+  my $self = shift;
+  my $svcdb = scalar(@_) ? shift : '';
+  my @pkg_svc = grep {
+    $_->quantity == 1
+    && ( $svcdb eq $_->part_svc->svcdb || !$svcdb )
+  } $self->pkg_svc;
+  return '' if scalar(@pkg_svc) != 1;
+  $pkg_svc[0]->svcpart;
+}
+
+=item payby
+
+Returns a list of the acceptable payment types for this package.  Eventually
+this should come out of a database table and be editable, but currently has the
+following logic instead;
+
+If the package has B<0> setup and B<0> recur, the single item B<BILL> is
+returned, otherwise, the single item B<CARD> is returned.
+
+(CHEK?  LEC?  Probably shouldn't accept those by default, prone to abuse)
+
+=cut
+
+sub payby {
+  my $self = shift;
+  #if ( $self->setup == 0 && $self->recur == 0 ) {
+  if (    $self->setup =~ /^\s*0+(\.0*)?\s*$/
+       && $self->recur =~ /^\s*0+(\.0*)?\s*$/ ) {
+    ( 'BILL' );
+  } else {
+    ( 'CARD' );
+  }
+}
+
+=back
+
+=head1 BUGS
+
+The delete method is unimplemented.
+
+setup and recur semantics are not yet defined (and are implemented in
+FS::cust_bill.  hmm.).
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_pkg>, L<FS::type_pkgs>, L<FS::pkg_svc>, L<Safe>.
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pop_local.pm b/FS/FS/part_pop_local.pm
new file mode 100644 (file)
index 0000000..0b7cdf6
--- /dev/null
@@ -0,0 +1,116 @@
+package FS::part_pop_local;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record; # qw( qsearchs );
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_pop_local - Object methods for part_pop_local records
+
+=head1 SYNOPSIS
+
+  use FS::part_pop_local;
+
+  $record = new FS::part_pop_local \%hash;
+  $record = new FS::part_pop_local { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_pop_local object represents a local call area.  Each
+FS::part_pop_local record maps a NPA/NXX (area code and exchange) to the POP
+(see L<FS::svc_acct_pop>) which is a local call.  FS::part_pop_local inherits
+from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item localnum - primary key (assigned automatically for new accounts)
+
+=item popnum - see L<FS::svc_acct_pop>
+
+=item city
+
+=item state
+
+=item npa - area code
+
+=item nxx - exchange
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new point of presence (if only it were that easy!).  To add the 
+point of presence to the database, see L<"insert">.
+
+=cut
+
+sub table { 'part_pop_local'; }
+
+=item insert
+
+Adds this point of presence to the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item delete
+
+Removes this point of presence from the database.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid point of presence.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+    $self->ut_numbern('localnum')
+      or $self->ut_numbern('popnum')
+      or $self->ut_text('city')
+      or $self->ut_text('state')
+      or $self->ut_number('npa')
+      or $self->ut_number('nxx')
+  ;
+
+}
+
+=back
+
+=head1 VERSION
+
+$Id: part_pop_local.pm,v 1.1 2001-09-26 09:17:06 ivan Exp $
+
+=head1 BUGS
+
+US/CA-centric.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::svc_acct_pop>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_referral.pm b/FS/FS/part_referral.pm
new file mode 100644 (file)
index 0000000..23885df
--- /dev/null
@@ -0,0 +1,116 @@
+package FS::part_referral;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_referral - Object methods for part_referral objects
+
+=head1 SYNOPSIS
+
+  use FS::part_referral;
+
+  $record = new FS::part_referral \%hash
+  $record = new FS::part_referral { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_referral represents a advertising source - where a customer heard
+of your services.  This can be used to track the effectiveness of a particular
+piece of advertising, for example.  FS::part_referral inherits from FS::Record.
+The following fields are currently supported:
+
+=over 4
+
+=item refnum - primary key (assigned automatically for new referrals)
+
+=item referral - Text name of this advertising source
+
+=back
+
+=head1 NOTE
+
+These were called B<referrals> before version 1.4.0 - the name was changed
+so as not to be confused with the new customer-to-customer referrals.
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new advertising source.  To add the referral to the database, see
+L<"insert">.
+
+=cut
+
+sub table { 'part_referral'; }
+
+=item insert
+
+Adds this advertising source to the database.  If there is an error, returns
+the error, otherwise returns false.
+
+=item delete
+
+Currently unimplemented.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  return "Can't (yet?) delete part_referral records";
+  #need to make sure no customers have this referral!
+}
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid advertising source.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert and
+replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  $self->ut_numbern('refnum')
+    || $self->ut_text('referral')
+  ;
+}
+
+=back
+
+=head1 BUGS
+
+The delete method is unimplemented.
+
+`Advertising source'.  Yes, it's a sucky name.  The only other ones I could
+come up with were "Marketing channel" and "Heard Abouts" and those are
+definately both worse.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_main>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_router_field.pm b/FS/FS/part_router_field.pm
new file mode 100755 (executable)
index 0000000..73ca50f
--- /dev/null
@@ -0,0 +1,134 @@
+package FS::part_router_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::router_field;
+use FS::router;
+
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_router_field - Object methods for part_router_field records
+
+=head1 SYNOPSIS
+
+  use FS::part_router_field;
+
+  $record = new FS::part_router_field \%hash;
+  $record = new FS::part_router_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+A part_router_field represents an xfield definition for routers.  For more
+information on xfields, see L<FS::part_sb_field>.
+
+The following fields are supported:
+
+=over 4
+
+=item routerfieldpart - primary key (assigned automatically)
+
+=item name - name of field
+
+=item length
+
+=item check_block
+
+=item list_source
+
+(See L<FS::part_sb_field> for details on these fields.)
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'part_router_field'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+  my $error = '';
+
+  $self->name =~ /^([a-z0-9_\-\.]{1,15})$/i
+    or return "Invalid field name for part_router_field";
+
+  ''; #no error
+}
+
+=item list_values
+
+Equivalent to "eval($part_router_field->list_source)".
+
+=cut
+
+sub list_values {
+  my $self = shift;
+  return () unless $self->list_source;
+  my @opts = eval($self->list_source);
+  if($@) { 
+    warn $@;
+    return ();
+  } else { 
+    return @opts;
+  }
+}
+
+=back
+
+=head1 VERSION
+
+$Id: 
+
+=head1 BUGS
+
+Needless duplication of much of FS::part_sb_field, with the result that most of
+the warnings about it apply here also.
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::router, FS::router_field,  schema.html
+from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_sb_field.pm b/FS/FS/part_sb_field.pm
new file mode 100755 (executable)
index 0000000..8dca946
--- /dev/null
@@ -0,0 +1,267 @@
+package FS::part_sb_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_sb_field - Object methods for part_sb_field records
+
+=head1 SYNOPSIS
+
+  use FS::part_sb_field;
+
+  $record = new FS::part_sb_field \%hash;
+  $record = new FS::part_sb_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_sb_field object represents an extended field (xfield) definition 
+for svc_broadband's sb_field mechanism (see L<FS::svc_broadband>).  
+FS::part_sb_field inherits from FS::Record.  The following fields are 
+currently supported:
+
+=over 2
+
+=item sbfieldpart - primary key (assigned automatically)
+
+=item name - name of the field
+
+=item svcpart - service type for which this field is available (see L<FS::part_svc>)
+
+=item length - length of the contents of the field (see note #1)
+
+=item check_block - validation routine (see note #2)
+
+=item list_source - enumeration routine (see note #3)
+
+=back
+
+=head1 BACKGROUND
+
+Broadband services, unlike dialup services, are provided over a wide 
+variety of physical media (DSL, wireless, cable modems, digital circuits) 
+and network architectures (Ethernet, PPP, ATM).  For many of these access 
+mechanisms, adding a new customer requires knowledge of some properties 
+of the physical connection (circuit number, the type of CPE in use, etc.).
+It is unreasonable to expect ISPs to alter Freeside's schema (and the 
+associated library and UI code) to make each of these parameters a field in 
+svc_broadband.
+
+Hence sb_field and part_sb_field.  They allow the Freeside administrator to
+define 'extended fields' ('xfields') associated with svc_broadband records.
+These are I<not> processed in any way by Freeside itself; they exist solely for
+use by exports (see L<FS::part_export>) and technical support staff.
+
+For a parallel mechanism (at the per-router level rather than per-service), 
+see L<FS::part_router_field>.
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'part_sb_field'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+  my $error = '';
+
+  $error = $self->ut_numbern('svcpart');
+  return $error if $error;
+
+  unless (qsearchs('part_svc', { svcpart => $self->svcpart }))
+    { return "Unknown svcpart: " . $self->svcpart;}
+
+  $self->name =~ /^([a-z0-9_\-\.]{1,15})$/i
+    or return "Invalid field name for part_sb_field";
+
+  #How to check input_block, display_block, and check_block?
+
+  ''; #no error
+}
+
+=item list_values
+
+If the I<list_source> field is set, this method eval()s it and 
+returns its output.  If the field is empty, list_values returns 
+an empty list.
+
+Any arguments passed to this method will be received by the list_source 
+code, but this behavior is a fortuitous accident and may be removed in 
+the future.
+
+=cut
+
+sub list_values {
+  my $self = shift;
+  return () unless $self->list_source;
+
+  my @opts = eval($self->list_source);
+  if($@) {
+    warn $@;
+    return ();
+  } else {
+    return @opts;
+  }
+}
+
+=item part_svc
+
+Returns the FS::part_svc object associated with this field definition.
+
+=cut
+
+sub part_svc {
+  my $self = shift;
+  return qsearchs('part_svc', { svcpart => $self->svcpart });
+}
+
+=back
+
+=head1 VERSION
+
+$Id: 
+
+=head1 NOTES
+
+=over
+
+=item 1.
+
+The I<length> field is not enforced.  It provides a hint to UI
+code about how to display the field on a form.  If you want to enforce a
+minimum or maximum length for a field, use a I<check_block>.
+
+=item 2.
+
+The check_block mechanism used here as well as in
+FS::part_router_field allows the user to define validation rules.
+
+When FS::sb_field::check is called, the proposed value of the xfield is
+assigned to $_.  The check_block is then eval()'d and its return value
+captured.  If the return value is false (empty/zero/undef), $_ is then assigned
+back into the field and stored in the database.
+
+Therefore a check_block can do three different things with the value: allow
+it, allow it with a modification, or reject it.  This is very flexible, but
+somewhat dangerous.  Some warnings:
+
+=over 2
+
+=item *
+
+Assume that $_ has had I<no> error checking prior to the
+check_block.  That's what the check_block is for, after all.  It could
+contain I<anything>: evil shell commands in backquotes, 100kb JPEG images,
+the Klez virus, whatever.
+
+=item *
+
+If your check_block modifies the input value, it should probably
+produce a value that wouldn't be modified by going through the same
+check_block again.  (That is, it should map input values into its own
+eigenspace.)  The reason is that if someone calls $new->replace($old),
+where $new and $old contain the same value for the field, they probably
+want the field to keep its old value, not to get transformed by the
+check_block again.  So don't do silly things like '$_++' or
+'tr/A-Za-z/a-zA-Z/'.
+
+=item *
+
+Don't alter the contents of the database.  I<Reading> the database
+is perfectly reasonable, but writing to it is a bad idea.  Remember that
+check() might get called more than once, as described above.
+
+=item *
+
+The check_block probably won't even get called if the user submits
+an I<empty> sb_field.  So at present, you can't set up a default value with
+something like 's/^$/foo/'.  Conversely, don't replace the submitted value
+with an empty string.  It probably will get stored, but might be deleted at
+any time.
+
+=back
+
+=item 3.
+
+The list_source mechanism is a UI hint (like length) to generate
+drop-down or list boxes.  If list_source contains a value, the UI code can
+eval() it and use the results as the options on the list.
+
+Note 'can'.  This is not a substitute for check_block.  The HTML interface
+currently requires that the user pick one of the options on the list
+because that's the way HTML drop-down boxes work, but in the future the UI
+code might add an 'Other (please specify)' option and a text box so that
+the user can enter something else.  Or it might ignore list_source and just
+generate a text box.  Or the interface might be rewritten in MS Access,
+where drop-down boxes have text boxes built in.  Data validation is the job
+of check(), not the front end.
+
+Note also that a list of literals evaluates to itself, so a list_source
+like
+
+C<('Windows', 'MacOS', 'Linux')>
+
+or
+
+C<qw(Windows MacOS Linux)>
+
+means exactly what you'd think.
+
+=head1 BUGS
+
+The lack of any way to do default values.  We might add this as another UI
+hint (since, for the most part, it's the UI's job to figure out which fields
+have had values entered into them).  In fact, there are lots of things we
+should add as UI hints.
+
+Oh, and the documentation is probably full of lies.
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::sb_field, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm
new file mode 100644 (file)
index 0000000..63bc2ad
--- /dev/null
@@ -0,0 +1,324 @@
+package FS::part_svc;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs fields dbh );
+use FS::part_svc_column;
+use FS::part_export;
+use FS::export_svc;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::part_svc - Object methods for part_svc objects
+
+=head1 SYNOPSIS
+
+  use FS::part_svc;
+
+  $record = new FS::part_svc \%hash
+  $record = new FS::part_svc { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_svc represents a service definition.  FS::part_svc inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item svcpart - primary key (assigned automatically for new service definitions)
+
+=item svc - text name of this service definition
+
+=item svcdb - table used for this service.  See L<FS::svc_acct>,
+L<FS::svc_domain>, and L<FS::svc_forward>, among others.
+
+=item disabled - Disabled flag, empty or `Y'
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new service definition.  To add the service definition to the
+database, see L<"insert">.
+
+=cut
+
+sub table { 'part_svc'; }
+
+=item insert EXTRA_FIELDS_ARRAYREF
+
+Adds this service definition to the database.  If there is an error, returns
+the error, otherwise returns false.
+
+TODOC:
+
+=item I<svcdb>__I<field> - Default or fixed value for I<field> in I<svcdb>.
+
+=item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null, `D' for default, or `F' for fixed
+
+TODOC: EXTRA_FIELDS_ARRAYREF
+
+=cut
+
+sub insert {
+  my $self = shift;
+  my @fields = ();
+  @fields = @{shift(@_)} if @_;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  my $svcdb = $self->svcdb;
+#  my @rows = map { /^${svcdb}__(.*)$/; $1 }
+#    grep ! /_flag$/,
+#      grep /^${svcdb}__/,
+#        fields('part_svc');
+  foreach my $field (
+    grep { $_ ne 'svcnum'
+           && defined( $self->getfield($svcdb.'__'.$_.'_flag') )
+         } (fields($svcdb), @fields)
+  ) {
+    my $part_svc_column = $self->part_svc_column($field);
+    my $previous = qsearchs('part_svc_column', {
+      'svcpart'    => $self->svcpart,
+      'columnname' => $field,
+    } );
+
+    my $flag = $self->getfield($svcdb.'__'.$field.'_flag');
+    if ( uc($flag) =~ /^([DF])$/ ) {
+      $part_svc_column->setfield('columnflag', $1);
+      $part_svc_column->setfield('columnvalue',
+        $self->getfield($svcdb.'__'.$field)
+      );
+      if ( $previous ) {
+        $error = $part_svc_column->replace($previous);
+      } else {
+        $error = $part_svc_column->insert;
+      }
+    } else {
+      $error = $previous ? $previous->delete : '';
+    }
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item delete
+
+Currently unimplemented.
+
+=cut
+
+sub delete {
+  return "Can't (yet?) delete service definitions.";
+# check & make sure the svcpart isn't in cust_svc or pkg_svc (in any packages)?
+}
+
+=item replace OLD_RECORD [ '1.3-COMPAT' [ , EXTRA_FIELDS_ARRAYREF ] ]
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+TODOC: 1.3-COMPAT
+
+TODOC: EXTRA_FIELDS_ARRAYREF
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+
+  return "Can't change svcdb for an existing service definition!"
+    unless $old->svcdb eq $new->svcdb;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $new->SUPER::replace( $old );
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( @_ && $_[0] eq '1.3-COMPAT' ) {
+    shift;
+    my @fields = ();
+    @fields = @{shift(@_)} if @_;
+
+    my $svcdb = $new->svcdb;
+    foreach my $field (
+      grep { $_ ne 'svcnum'
+             && defined( $new->getfield($svcdb.'__'.$_.'_flag') )
+           } (fields($svcdb),@fields)
+    ) {
+      my $part_svc_column = $new->part_svc_column($field);
+      my $previous = qsearchs('part_svc_column', {
+        'svcpart'    => $new->svcpart,
+        'columnname' => $field,
+      } );
+
+      my $flag = $new->getfield($svcdb.'__'.$field.'_flag');
+      if ( uc($flag) =~ /^([DF])$/ ) {
+        $part_svc_column->setfield('columnflag', $1);
+        $part_svc_column->setfield('columnvalue',
+          $new->getfield($svcdb.'__'.$field)
+        );
+        if ( $previous ) {
+          $error = $part_svc_column->replace($previous);
+        } else {
+          $error = $part_svc_column->insert;
+        }
+      } else {
+        $error = $previous ? $previous->delete : '';
+      }
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  } else {
+    $dbh->rollback if $oldAutoCommit;
+    return 'non-1.3-COMPAT interface not yet written';
+    #not yet implemented
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item check
+
+Checks all fields to make sure this is a valid service definition.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+  my $recref = $self->hashref;
+
+  my $error;
+  $error=
+    $self->ut_numbern('svcpart')
+    || $self->ut_text('svc')
+    || $self->ut_alpha('svcdb')
+    || $self->ut_enum('disabled', [ '', 'Y' ] )
+  ;
+  return $error if $error;
+
+  my @fields = eval { fields( $recref->{svcdb} ) }; #might die
+  return "Unknown svcdb!" unless @fields;
+
+  ''; #no error
+}
+
+=item part_svc_column COLUMNNAME
+
+Returns the part_svc_column object (see L<FS::part_svc_column>) for the given
+COLUMNNAME, or a new part_svc_column object if none exists.
+
+=cut
+
+sub part_svc_column {
+  my( $self, $columnname) = @_;
+  $self->svcpart &&
+    qsearchs('part_svc_column',  {
+                                   'svcpart'    => $self->svcpart,
+                                   'columnname' => $columnname,
+                                 }
+  ) or new FS::part_svc_column {
+                                 'svcpart'    => $self->svcpart,
+                                 'columnname' => $columnname,
+                               };
+}
+
+=item all_part_svc_column
+
+=cut
+
+sub all_part_svc_column {
+  my $self = shift;
+  qsearch('part_svc_column', { 'svcpart' => $self->svcpart } );
+}
+
+=item part_export [ EXPORTTYPE ]
+
+Returns all exports (see L<FS::part_export>) for this service, or, if an
+export type is specified, only returns exports of the given type.
+
+=cut
+
+sub part_export {
+  my $self = shift;
+  my %search;
+  $search{'exporttype'} = shift if @_;
+  map { qsearchs('part_export', { 'exportnum' => $_->exportnum, %search } ) }
+    qsearch('export_svc', { 'svcpart' => $self->svcpart } );
+}
+
+=back
+
+=head1 BUGS
+
+Delete is unimplemented.
+
+The list of svc_* tables is hardcoded.  When svc_acct_pop is renamed, this
+should be fixed.
+
+all_part_svc_column method should be documented
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::part_svc_column>, L<FS::part_pkg>, L<FS::pkg_svc>,
+L<FS::cust_svc>, L<FS::svc_acct>, L<FS::svc_forward>, L<FS::svc_domain>,
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_svc_column.pm b/FS/FS/part_svc_column.pm
new file mode 100644 (file)
index 0000000..37e841e
--- /dev/null
@@ -0,0 +1,118 @@
+package FS::part_svc_column;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( fields );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::part_svc_column - Object methods for part_svc_column objects
+
+=head1 SYNOPSIS
+
+  use FS::part_svc_column;
+
+  $record = new FS::part_svc_column \%hash
+  $record = new FS::part_svc_column { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_svc_column record represents a service definition column
+constraint.  FS::part_svc_column inherits from FS::Record.  The following
+fields are currently supported:
+
+=over 4
+
+=item columnnum - primary key (assigned automatcially for new records)
+
+=item svcpart - service definition (see L<FS::part_svc>)
+
+=item columnname - column name in part_svc.svcdb table
+
+=item columnvalue - default or fixed value for the column
+
+=item columnflag - null, D or F
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new column constraint.  To add the column constraint to the database, see L<"insert">.
+
+=cut
+
+sub table { 'part_svc_column'; }
+
+=item insert
+
+Adds this service definition to the database.  If there is an error, returns
+the error, otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('columnnum')
+    || $self->ut_number('svcpart')
+    || $self->ut_alpha('columnname')
+    || $self->ut_anything('columnvalue')
+  ;
+  return $error if $error;
+
+  $self->columnflag =~ /^([DF])$/
+    or return "illegal columnflag ". $self->columnflag;
+  $self->columnflag(uc($1));
+
+  ''; #no error
+}
+
+=back
+
+=head1 VERSION
+
+$Id: part_svc_column.pm,v 1.1 2001-09-07 20:49:15 ivan Exp $
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::part_svc>, L<FS::part_pkg>, L<FS::pkg_svc>,
+L<FS::cust_svc>, L<FS::svc_acct>, L<FS::svc_forward>, L<FS::svc_domain>,
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_svc_router.pm b/FS/FS/part_svc_router.pm
new file mode 100755 (executable)
index 0000000..0b23ab5
--- /dev/null
@@ -0,0 +1,32 @@
+package FS::part_svc_router;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw(qsearchs);
+use FS::router;
+use FS::part_svc;
+
+@ISA = qw(FS::Record);
+
+sub table { 'part_svc_router'; }
+
+sub check {
+  my $self = shift;
+  my $error =
+    $self->ut_foreign_key('svcpart', 'part_svc', 'svcpart')
+    || $self->ut_foreign_key('routernum', 'router', 'routernum');
+  return $error if $error;
+  ''; #no error
+}
+
+sub router {
+  my $self = shift;
+  return qsearchs('router', { routernum => $self->routernum });
+}
+
+sub part_svc {
+  my $self = shift;
+  return qsearchs('part_svc', { svcpart => $self->svcpart });
+}
+
+1;
diff --git a/FS/FS/pkg_svc.pm b/FS/FS/pkg_svc.pm
new file mode 100644 (file)
index 0000000..3c544ff
--- /dev/null
@@ -0,0 +1,152 @@
+package FS::pkg_svc;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::part_pkg;
+use FS::part_svc;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::pkg_svc - Object methods for pkg_svc records
+
+=head1 SYNOPSIS
+
+  use FS::pkg_svc;
+
+  $record = new FS::pkg_svc \%hash;
+  $record = new FS::pkg_svc { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $part_pkg = $record->part_pkg;
+
+  $part_svc = $record->part_svc;
+
+=head1 DESCRIPTION
+
+An FS::pkg_svc record links a billing item definition (see L<FS::part_pkg>) to
+a service definition (see L<FS::part_svc>).  FS::pkg_svc inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item pkgpart - Billing item definition (see L<FS::part_pkg>)
+
+=item svcpart - Service definition (see L<FS::part_svc>)
+
+=item quantity - Quantity of this service definition that this billing item
+definition includes
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'pkg_svc'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+
+  return "Can't change pkgpart!" if $old->pkgpart != $new->pkgpart;
+  return "Can't change svcpart!" if $old->svcpart != $new->svcpart;
+
+  $new->SUPER::replace($old);
+}
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error;
+  $error =
+    $self->ut_number('pkgpart')
+    || $self->ut_number('svcpart')
+    || $self->ut_number('quantity')
+  ;
+  return $error if $error;
+
+  return "Unknown pkgpart!" unless $self->part_pkg;
+  return "Unknown svcpart!" unless $self->part_svc;
+
+  ''; #no error
+}
+
+=item part_pkg
+
+Returns the FS::part_pkg object (see L<FS::part_pkg>).
+
+=cut
+
+sub part_pkg {
+  my $self = shift;
+  qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
+}
+
+=item part_svc
+
+Returns the FS::part_svc object (see L<FS::part_svc>).
+
+=cut
+
+sub part_svc {
+  my $self = shift;
+  qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
+}
+
+=back
+
+=head1 VERSION
+
+$Id: pkg_svc.pm,v 1.3 2002-06-10 01:39:50 khoff Exp $
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::part_pkg>, L<FS::part_svc>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/port.pm b/FS/FS/port.pm
new file mode 100644 (file)
index 0000000..13455ca
--- /dev/null
@@ -0,0 +1,160 @@
+package FS::port;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::nas;
+use FS::session;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::port - Object methods for port records
+
+=head1 SYNOPSIS
+
+  use FS::port;
+
+  $record = new FS::port \%hash;
+  $record = new FS::port { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $session = $port->session;
+
+=head1 DESCRIPTION
+
+An FS::port object represents an individual port on a NAS.  FS::port inherits
+from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item portnum - primary key
+
+=item ip - IP address of this port
+
+=item nasport - port number on the NAS
+
+=item nasnum - NAS this port is on - see L<FS::nas>
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new port.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'port'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+  my $error =
+    $self->ut_numbern('portnum')
+    || $self->ut_ipn('ip')
+    || $self->ut_numbern('nasport')
+    || $self->ut_number('nasnum');
+  ;
+  return $error if $error;
+  return "Either ip or nasport must be specified"
+    unless $self->ip || $self->nasport;
+  return "Unknown nasnum"
+    unless qsearchs('nas', { 'nasnum' => $self->nasnum } );
+  ''; #no error
+}
+
+=item session
+
+Returns the currently open session on this port, or if no session is currently
+open, the most recent session.  See L<FS::session>.
+
+=cut
+
+sub session {
+  my $self = shift;
+  qsearchs('session', { 'portnum' => $self->portnum }, '*',
+                     'ORDER BY login DESC LIMIT 1' );
+}
+
+=back
+
+=head1 VERSION
+
+$Id: port.pm,v 1.5 2001-02-14 04:33:06 ivan Exp $
+
+=head1 BUGS
+
+The author forgot to customize this manpage.
+
+The session method won't deal well if you have multiple open sessions on a
+port, for example if your RADIUS server drops B<stop> records.  Suggestions for
+how to deal with this sort of lossage welcome; should we close the session
+when we get a new session on that port?  Tag it as invalid somehow?  Close it
+one second after it was opened?  *sigh*  Maybe FS::session shouldn't let you
+create overlapping sessions, at least folks will find out their logging is
+dropping records.
+
+If you think the above refers multiple user logins you need to read the
+manpages again.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/prepay_credit.pm b/FS/FS/prepay_credit.pm
new file mode 100644 (file)
index 0000000..7ed9b83
--- /dev/null
@@ -0,0 +1,126 @@
+package FS::prepay_credit;
+
+use strict;
+use vars qw( @ISA );
+#use FS::Record qw( qsearch qsearchs );
+use FS::Record qw();
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::prepay_credit - Object methods for prepay_credit records
+
+=head1 SYNOPSIS
+
+  use FS::prepay_credit;
+
+  $record = new FS::prepay_credit \%hash;
+  $record = new FS::prepay_credit {
+    'identifier' => '4198123455512121'
+    'amount'     => '19.95',
+  };
+
+  $record = new FS::prepay_credit {
+    'identifier' => '4198123455512121'
+    'seconds'    => '7200',
+  };
+
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::table_name object represents an pre--paid credit, such as a pre-paid
+"calling card".  FS::prepay_credit inherits from FS::Record.  The following
+fields are currently supported:
+
+=over 4
+
+=item field - description
+
+=item identifier - identifier entered by the user to receive the credit
+
+=item amount - amount of the credit
+
+=item seconds - time amount of credit (see L<FS::svc_acct/seconds>)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new pre-paid credit.  To add the example to the database, see
+L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'prepay_credit'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+=item check
+
+Checks all fields to make sure this is a valid pre-paid credit.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $identifier = $self->identifier;
+  $identifier =~ s/\W//g; #anything else would just confuse things
+  $self->identifier($identifier);
+
+  $self->ut_numbern('prepaynum')
+  || $self->ut_alpha('identifier')
+  || $self->ut_money('amount')
+  || $self->utnumbern('seconds')
+  ;
+
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_acct>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/queue.pm b/FS/FS/queue.pm
new file mode 100644 (file)
index 0000000..d35dc88
--- /dev/null
@@ -0,0 +1,401 @@
+package FS::queue;
+
+use strict;
+use vars qw( @ISA @EXPORT_OK $conf $jobnums);
+use Exporter;
+use FS::UID;
+use FS::Conf;
+use FS::Record qw( qsearch qsearchs dbh );
+#use FS::queue;
+use FS::queue_arg;
+use FS::queue_depend;
+use FS::cust_svc;
+
+@ISA = qw(FS::Record);
+@EXPORT_OK = qw( joblisting );
+
+$FS::UID::callback{'FS::queue'} = sub {
+  $conf = new FS::Conf;
+};
+
+$jobnums = '';
+
+=head1 NAME
+
+FS::queue - Object methods for queue records
+
+=head1 SYNOPSIS
+
+  use FS::queue;
+
+  $record = new FS::queue \%hash;
+  $record = new FS::queue { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::queue object represents an queued job.  FS::queue inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item jobnum - primary key
+
+=item job - fully-qualified subroutine name
+
+=item status - job status
+
+=item statustext - freeform text status message
+
+=item _date - UNIX timestamp
+
+=item svcnum - optional link to service (see L<FS::cust_svc>)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new job.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'queue'; }
+
+=item insert [ ARGUMENT, ARGUMENT... ]
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+If any arguments are supplied, a queue_arg record for each argument is also
+created (see L<FS::queue_arg>).
+
+=cut
+
+#false laziness w/part_export.pm
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  foreach my $arg ( @_ ) {
+    my $queue_arg = new FS::queue_arg ( {
+      'jobnum' => $self->jobnum,
+      'arg'    => $arg,
+    } );
+    $error = $queue_arg->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  push @$jobnums, $self->jobnum if $jobnums;
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+=item delete
+
+Delete this record from the database.  Any corresponding queue_arg records are
+deleted as well
+
+=cut
+
+sub delete {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my @del = qsearch( 'queue_arg', { 'jobnum' => $self->jobnum } );
+  push @del, qsearch( 'queue_depend', { 'depend_jobnum' => $self->jobnum } );
+
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  foreach my $del ( @del ) {
+    $error = $del->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid job.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+  my $error =
+    $self->ut_numbern('jobnum')
+    || $self->ut_anything('job')
+    || $self->ut_numbern('_date')
+    || $self->ut_enum('status',['', qw( new locked failed )])
+    || $self->ut_anything('statustext')
+    || $self->ut_numbern('svcnum')
+  ;
+  return $error if $error;
+
+  $error = $self->ut_foreign_keyn('svcnum', 'cust_svc', 'svcnum');
+  $self->svcnum('') if $error;
+
+  $self->status('new') unless $self->status;
+  $self->_date(time) unless $self->_date;
+
+  ''; #no error
+}
+
+=item args
+
+Returns a list of the arguments associated with this job.
+
+=cut
+
+sub args {
+  my $self = shift;
+  map $_->arg, qsearch( 'queue_arg',
+                        { 'jobnum' => $self->jobnum },
+                        '',
+                        'ORDER BY argnum'
+                      );
+}
+
+=item cust_svc
+
+Returns the FS::cust_svc object associated with this job, if any.
+
+=cut
+
+sub cust_svc {
+  my $self = shift;
+  qsearchs('cust_svc', { 'svcnum' => $self->svcnum } );
+}
+
+=item queue_depend
+
+Returns the FS::queue_depend objects associated with this job, if any.
+
+=cut
+
+sub queue_depend {
+  my $self = shift;
+  qsearch('queue_depend', { 'jobnum' => $self->jobnum } );
+}
+
+
+=item depend_insert OTHER_JOBNUM
+
+Inserts a dependancy for this job - it will not be run until the other job
+specified completes.  If there is an error, returns the error, otherwise
+returns false.
+
+When using job dependancies, you should wrap the insertion of all relevant jobs
+in a database transaction.  
+
+=cut
+
+sub depend_insert {
+  my($self, $other_jobnum) = @_;
+  my $queue_depend = new FS::queue_depend ( {
+    'jobnum'        => $self->jobnum,
+    'depend_jobnum' => $other_jobnum,
+  } );
+  $queue_depend->insert;
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item joblisting HASHREF NOACTIONS
+
+=cut
+
+sub joblisting {
+  my($hashref, $noactions) = @_;
+
+  use Date::Format;
+  use HTML::Entities;
+  use FS::CGI;
+
+  my @queue = qsearch( 'queue', $hashref );
+  return '' unless scalar(@queue);
+
+  my $p = FS::CGI::popurl(2);
+
+  my $html = qq!<FORM ACTION="$p/misc/queue.cgi" METHOD="POST">!.
+             FS::CGI::table(). <<END;
+      <TR>
+        <TH COLSPAN=2>Job</TH>
+        <TH>Args</TH>
+        <TH>Date</TH>
+        <TH>Status</TH>
+END
+  $html .= '<TH>Account</TH>' unless $hashref->{svcnum};
+  $html .= '</TR>';
+
+  my $dangerous = $conf->exists('queue_dangerous_controls');
+
+  my $areboxes = 0;
+
+  foreach my $queue ( sort { 
+    $a->getfield('jobnum') <=> $b->getfield('jobnum')
+  } @queue ) {
+    my $queue_hashref = $queue->hashref;
+    my $jobnum = $queue->jobnum;
+
+    my $args;
+    if ( $dangerous || $queue->job !~ /^FS::part_export::/ || !$noactions ) {
+      $args = encode_entities( join(' ',
+        map { length($_)<54 ? $_ : substr($_,0,32)."..."  } $queue->args #1&g
+      ) );
+    } else {
+      $args = '';
+    }
+
+    my $date = time2str( "%a %b %e %T %Y", $queue->_date );
+    my $status = $queue->status;
+    $status .= ': '. $queue->statustext if $queue->statustext;
+    my @queue_depend = $queue->queue_depend;
+    $status .= ' (waiting for '.
+               join(', ', map { $_->depend_jobnum } @queue_depend ). 
+               ')'
+      if @queue_depend;
+    my $changable = $dangerous
+         || ( ! $noactions && $status =~ /^failed/ || $status =~ /^locked/ );
+    if ( $changable ) {
+      $status .=
+        qq! (&nbsp;<A HREF="$p/misc/queue.cgi?jobnum=$jobnum&action=new">retry</A>&nbsp;|!.
+        qq!&nbsp;<A HREF="$p/misc/queue.cgi?jobnum=$jobnum&action=del">remove</A>&nbsp;)!;
+    }
+    my $cust_svc = $queue->cust_svc;
+
+    $html .= <<END;
+      <TR>
+        <TD>$jobnum</TD>
+        <TD>$queue_hashref->{job}</TD>
+        <TD>$args</TD>
+        <TD>$date</TD>
+        <TD>$status</TD>
+END
+
+    unless ( $hashref->{svcnum} ) {
+      my $account;
+      if ( $cust_svc ) {
+        my $table = $cust_svc->part_svc->svcdb;
+        my $label = ( $cust_svc->label )[1];
+        $account = qq!<A HREF="../view/$table.cgi?!. $queue->svcnum.
+                   qq!">$label</A>!;
+      } else {
+        $account = '';
+      }
+      $html .= "<TD>$account</TD>";
+    }
+
+    if ( $changable ) {
+      $areboxes=1;
+      $html .=
+        qq!<TD><INPUT NAME="jobnum$jobnum" TYPE="checkbox" VALUE="1"></TD>!;
+
+    }
+
+    $html .= '</TR>';
+
+}
+
+  $html .= '</TABLE>';
+
+  if ( $areboxes ) {
+    $html .= '<BR><INPUT TYPE="submit" NAME="action" VALUE="retry selected">'.
+             '<INPUT TYPE="submit" NAME="action" VALUE="remove selected"><BR>';
+  }
+
+  $html;
+
+}
+
+=back
+
+=head1 VERSION
+
+$Id: queue.pm,v 1.15 2002-07-02 06:48:59 ivan Exp $
+
+=head1 BUGS
+
+$jobnums global
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/queue_arg.pm b/FS/FS/queue_arg.pm
new file mode 100644 (file)
index 0000000..08fe473
--- /dev/null
@@ -0,0 +1,121 @@
+package FS::queue_arg;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::queue_arg - Object methods for queue_arg records
+
+=head1 SYNOPSIS
+
+  use FS::queue_arg;
+
+  $record = new FS::queue_arg \%hash;
+  $record = new FS::queue_arg { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::queue_arg object represents job argument.  FS::queue_arg inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item argnum - primary key
+
+=item jobnum - see L<FS::queue>
+
+=item arg - argument
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new argument.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'queue_arg'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid argument.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+  my $error =
+    $self->ut_numbern('argnum')
+    || $self->ut_numbern('jobnum')
+    || $self->ut_anything('arg')
+  ;
+  return $error if $error;
+
+  ''; #no error
+}
+
+=back
+
+=head1 VERSION
+
+$Id: queue_arg.pm,v 1.1 2001-09-11 00:08:18 ivan Exp $
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::queue>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/queue_depend.pm b/FS/FS/queue_depend.pm
new file mode 100644 (file)
index 0000000..4a4e3c5
--- /dev/null
@@ -0,0 +1,120 @@
+package FS::queue_depend;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+use FS::queue;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::queue_depend - Object methods for queue_depend records
+
+=head1 SYNOPSIS
+
+  use FS::queue_depend;
+
+  $record = new FS::queue_depend \%hash;
+  $record = new FS::queue_depend { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::queue_depend object represents an job dependancy.  FS::queue_depend
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item dependnum - primary key
+
+=item jobnum - source jobnum (see L<FS::queue>).
+
+=item depend_jobnum - dependancy jobnum (see L<FS::queue>)
+
+=back
+
+The job specified by B<jobnum> depends on the job specified B<depend_jobnum> -
+the B<jobnum> job will not be run until the B<depend_jobnum> job has completed
+sucessfully (or manually removed).
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new dependancy.  To add the dependancy to the database, see
+L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'queue_depend'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid dependancy.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  $self->ut_numbern('dependnum')
+    || $self->ut_foreign_key('jobnum',        'queue', 'jobnum')
+    || $self->ut_foreign_key('depend_jobnum', 'queue', 'jobnum')
+  ;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::queue>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/raddb.pm b/FS/FS/raddb.pm
new file mode 100644 (file)
index 0000000..a35a757
--- /dev/null
@@ -0,0 +1,1106 @@
+package FS::raddb;
+use vars qw(%attrib);
+
+%attrib = (
+  'usr_at_zip_output_filter' => 'USR-AT-Zip-Output-Filter',
+  'ms_filter' => 'MS-Filter',
+  'usr_blocks_received' => 'USR-Blocks-Received',
+  'shiva_called_number' => 'Shiva-Called-Number',
+  'annex_filter' => 'Annex-Filter',
+  'usr_channel_expansion' => 'USR-Channel-Expansion',
+  'session_timeout' => 'Session-Timeout',
+  'usr_simplified_mnp_level' => 'USR-Simplified-MNP-Levels',
+  'ascend_route_ipx' => 'Ascend-Route-IPX',
+  'annex_user_server_locati' => 'Annex-User-Server-Location',
+  'acc_callback_mode' => 'Acc-Callback-Mode',
+  'usr_filter_zones' => 'USR-Filter-Zones',
+  'ascend_session_svr_key' => 'Ascend-Session-Svr-Key',
+  'le_nat_tcp_session_timeo' => 'LE-NAT-TCP-Session-Timeout',
+  'ascend_ts_idle_limit' => 'Ascend-TS-Idle-Limit',
+  'usr_port_tap_priority' => 'USR-Port-Tap-Priority',
+  'ascend_private_route' => 'Ascend-Private-Route',
+  'prompt' => 'Prompt',
+  'acct_link_count' => 'Acct-Link-Count',
+  'login_lat_node' => 'Login-LAT-Node',
+  'usr_mbi_ct_pri_card_slot' => 'USR-Mbi_Ct_PRI_Card_Slot',
+  'erx_ingress_statistics' => 'ERX-Ingress-Statistics',
+  'annex_system_disc_reason' => 'Annex-System-Disc-Reason',
+  'usr_call_arrival_time' => 'USR-Call-Arrival-Time',
+  'ascend_disconnect_cause' => 'Ascend-Disconnect-Cause',
+  'ascend_user_acct_time' => 'Ascend-User-Acct-Time',
+  'ascend_appletalk_peer_mo' => 'Ascend-Appletalk-Peer-Mode',
+  'chap_challenge' => 'CHAP-Challenge',
+  'ascend_mpp_idle_percent' => 'Ascend-MPP-Idle-Percent',
+  'ascend_user_acct_port' => 'Ascend-User-Acct-Port',
+  'ascend_numbering_plan_id' => 'Ascend-Numbering-Plan-ID',
+  'ascend_access_intercept_' => 'Ascend-Access-Intercept-LEA',
+  'pvc_encapsulation_type' => 'PVC_Encapsulation_Type',
+  'ascend_bir_bridge_group' => 'Ascend-BIR-Bridge-Group',
+  'ascend_atm_group' => 'Ascend-ATM-Group',
+  'ascend_fr_svc_addr' => 'Ascend-FR-SVC-Addr',
+  'x_ascend_send_auth' => 'X-Ascend-Send-Auth',
+  'le_ip_pool' => 'LE-IP-Pool',
+  'annex_addr_resolution_se' => 'Annex-Addr-Resolution-Servers',
+  'usr_last_callers_number_' => 'USR-Last-Callers-Number-ANI',
+  'login_port' => 'Login-Port',
+  'ms_chap2_response' => 'MS-CHAP2-Response',
+  'annex_secondary_dns_serv' => 'Annex-Secondary-DNS-Server',
+  'ascend_ipsec_profile' => 'Ascend-IPSEC-Profile',
+  'usr_accm_type' => 'USR-ACCM-Type',
+  'simultaneous_use' => 'Simultaneous-Use',
+  'framed_protocol' => 'Framed-Protocol',
+  'ascend_recv_name' => 'Ascend-Recv-Name',
+  'usr_call_connecting_time' => 'USR-Call-Connecting-Time',
+  'tunnel_remote_name' => 'Tunnel_Remote_Name',
+  'usr_vts_session_key' => 'USR-VTS-Session-Key',
+  'ascend_fr_dce_n393' => 'Ascend-FR-DCE-N393',
+  'login_host' => 'Login-Host',
+  'usr_reply_script3' => 'USR-Reply-Script3',
+  'ascend_pppoe_enable' => 'Ascend-PPPoE-Enable',
+  'annex_primary_dns_server' => 'Annex-Primary-DNS-Server',
+  'x_ascend_bridge_address' => 'X-Ascend-Bridge-Address',
+  'usr_number_of_link_naks' => 'USR-Number-of-Link-NAKs',
+  'annex_cli_command' => 'Annex-CLI-Command',
+  'usr_pw_framed_routing_v2' => 'USR-PW_Framed_Routing_V2',
+  'usr_tunnel_switch_endpoi' => 'USR-Tunnel-Switch-Endpoint',
+  'ascend_call_by_call' => 'Ascend-Call-By-Call',
+  'ascend_first_dest' => 'Ascend-First-Dest',
+  'usr_appletalk_network_ra' => 'USR-Appletalk-Network-Range',
+  'annex_tunnel_authen_type' => 'Annex-Tunnel-Authen-Type',
+  'sql_user_name' => 'SQL-User-Name',
+  'erx_secondary_dns' => 'ERX-Secondary-Dns',
+  'h323_return_code' => 'h323-return-code',
+  'annex_host_allow' => 'Annex-Host-Allow',
+  'x_ascend_require_auth' => 'X-Ascend-Require-Auth',
+  'le_ipsec_deny_action' => 'LE-IPSec-Deny-Action',
+  'annex_edo' => 'Annex-EDO',
+  'acct_delay_time' => 'Acct-Delay-Time',
+  'ascend_call_block_durati' => 'Ascend-Call-Block-Duration',
+  'login_tcp_port' => 'Login-TCP-Port',
+  'ascend_temporary_rtes' => 'Ascend-Temporary-Rtes',
+  'ascend_dialed_number' => 'Ascend-Dialed-Number',
+  'x_ascend_dec_channel_cou' => 'X-Ascend-Dec-Channel-Count',
+  'ascend_fr_dlci' => 'Ascend-FR-DLCI',
+  'annex_modem_disc_reason' => 'Annex-Modem-Disc-Reason',
+  'x_ascend_receive_secret' => 'X-Ascend-Receive-Secret',
+  'char_noecho' => 'Char-Noecho',
+  'ascend_pri_number_type' => 'Ascend-PRI-Number-Type',
+  'ascend_dsl_upstream_limi' => 'Ascend-Dsl-Upstream-Limit',
+  'x_ascend_modem_shelfno' => 'X-Ascend-Modem-ShelfNo',
+  'prefix' => 'Prefix',
+  'usr_rad_dvmrp_metric' => 'USR-Rad-Dvmrp-Metric',
+  'usr_ip_saa_filter' => 'USR-IP-SAA-Filter',
+  'ms_link_utilization_thre' => 'MS-Link-Utilization-Threshold',
+  'ascend_home_network_name' => 'Ascend-Home-Network-Name',
+  'acc_customer_id' => 'Acc-Customer-Id',
+  'message_authenticator' => 'Message-Authenticator',
+  'ascend_secondary_home_ag' => 'Ascend-Secondary-Home-Agent',
+  'x_ascend_pre_output_octe' => 'X-Ascend-Pre-Output-Octets',
+  'usr_multicast_forwarding' => 'USR-Multicast-Forwarding',
+  'ascend_call_direction' => 'Ascend-Call-Direction',
+  'acc_connect_rx_speed' => 'Acc-Connect-Rx-Speed',
+  'ascend_force_56' => 'Ascend-Force-56',
+  'usr_harc_disconnect_code' => 'USR-HARC-Disconnect-Code',
+  'shasta_service_profile' => 'Shasta-Service-Profile',
+  'cisco_maximum_time' => 'Cisco-Maximum-Time',
+  'usr_tunnel_auth_hostname' => 'USR-Tunnel-Auth-Hostname',
+  'ascend_client_assign_win' => 'Ascend-Client-Assign-WINS',
+  'acc_modem_modulation_typ' => 'Acc-Modem-Modulation-Type',
+  'acc_ip_gateway_pri' => 'Acc-Ip-Gateway-Pri',
+  'ascend_bridge_address' => 'Ascend-Bridge-Address',
+  'x_ascend_fr_link_mgt' => 'X-Ascend-FR-Link-Mgt',
+  'ascend_handle_ipx' => 'Ascend-Handle-IPX',
+  'ascend_x25_pad_alias_2' => 'Ascend-X25-Pad-Alias-2',
+  'ascend_group' => 'Ascend-Group',
+  'ascend_dsl_rate_type' => 'Ascend-Dsl-Rate-Type',
+  'ascend_require_auth' => 'Ascend-Require-Auth',
+  'x_ascend_billing_number' => 'X-Ascend-Billing-Number',
+  'usr_orig_nas_type' => 'USR-Orig-NAS-Type',
+  'ascend_remote_fw' => 'Ascend-Remote-FW',
+  'acct_output_packets' => 'Acct-Output-Packets',
+  'lm_password' => 'LM-Password',
+  'tunnel_window' => 'Tunnel_Window',
+  'x_ascend_link_compressio' => 'X-Ascend-Link-Compression',
+  'x_ascend_base_channel_co' => 'X-Ascend-Base-Channel-Count',
+  'cisco_avpair' => 'Cisco-AVPair',
+  'shiva_event_flags' => 'Shiva-Event-Flags',
+  'usr_number_of_rings_limi' => 'USR-Number-of-Rings-Limit',
+  'ascend_ts_idle_mode' => 'Ascend-TS-Idle-Mode',
+  'ascend_bi_directional_au' => 'Ascend-Bi-Directional-Auth',
+  'state' => 'State',
+  'usr_keypress_timeout' => 'USR-Keypress-Timeout',
+  'usr_pw_vpn_neighbor' => 'USR-PW_VPN_Neighbor',
+  'ldap_userdn' => 'Ldap-UserDn',
+  'x_ascend_fr_n391' => 'X-Ascend-FR-N391',
+  'ascend_tunneling_protoco' => 'Ascend-Tunneling-Protocol',
+  'x_ascend_fr_direct' => 'X-Ascend-FR-Direct',
+  'nas_ip_address' => 'NAS-IP-Address',
+  'usr_call_end_time' => 'USR-Call-End-Time',
+  'tunnel_algorithm' => 'Tunnel_Algorithm',
+  'usr_vpn_encrypter' => 'USR-VPN-Encrypter',
+  'ascend_atm_connect_group' => 'Ascend-ATM-Connect-Group',
+  'x_ascend_ft1_caller' => 'X-Ascend-FT1-Caller',
+  'login_callback_number' => 'Login-Callback-Number',
+  'usr_ip_rip_input_filter' => 'USR-IP-RIP-Input-Filter',
+  'usr_rmmie_last_update_ev' => 'USR-RMMIE-Last-Update-Event',
+  'h323_disconnect_cause' => 'h323-disconnect-cause',
+  'x_ascend_handle_ipx' => 'X-Ascend-Handle-IPX',
+  'usr_igmp_version' => 'USR-IGMP-Version',
+  'usr_imsi' => 'USR-IMSI',
+  'group_name' => 'Group-Name',
+  'usr_nas_type' => 'USR-NAS-Type',
+  'ascend_ip_tos' => 'Ascend-IP-TOS',
+  'x_ascend_token_immediate' => 'X-Ascend-Token-Immediate',
+  'ascend_private_route_tab' => 'Ascend-Private-Route-Table-ID',
+  'ms_chap2_cpw' => 'MS-CHAP2-CPW',
+  'tunnel_session_auth_ctx' => 'Tunnel_Session_Auth_Ctx',
+  'usr_mobile_numbytes_rxed' => 'USR-Mobile-NumBytes-Rxed',
+  'usr_mbi_ct_tdm_time_slot' => 'USR-Mbi_Ct_TDM_Time_Slot',
+  'ascend_x25_nui' => 'Ascend-X25-Nui',
+  'x_ascend_first_dest' => 'X-Ascend-First-Dest',
+  'x_ascend_num_in_multilin' => 'X-Ascend-Num-In-Multilink',
+  'usr_send_password' => 'USR-Send-Password',
+  'x_ascend_fr_t391' => 'X-Ascend-FR-T391',
+  'acct_input_octets' => 'Acct-Input-Octets',
+  'bridge_group' => 'Bridge_Group',
+  'annex_sec_profile_index' => 'Annex-Sec-Profile-Index',
+  'acc_dns_server_pri' => 'Acc-Dns-Server-Pri',
+  'ms_acct_auth_type' => 'MS-Acct-Auth-Type',
+  'tunnel_password' => 'Tunnel-Password',
+  'usr_reply_script5' => 'USR-Reply-Script5',
+  'shiva_links_in_bundle' => 'Shiva-Links-In-Bundle',
+  'ascend_fr_profile_name' => 'Ascend-FR-Profile-Name',
+  'ascend_mtu' => 'Ascend-MTU',
+  'cisco_ppp_async_map' => 'Cisco-PPP-Async-Map',
+  'cisco_num_in_multilink' => 'Cisco-Num-In-Multilink',
+  'usr_mobile_ip_address' => 'USR-Mobile-IP-Address',
+  'ascend_bridge' => 'Ascend-Bridge',
+  'x_ascend_presession_time' => 'X-Ascend-PreSession-Time',
+  'tunnel_cmd_timeout' => 'Tunnel_Cmd_Timeout',
+  'ascend_multicast_client' => 'Ascend-Multicast-Client',
+  'tunnel_private_group_id' => 'Tunnel-Private-Group-Id',
+  'usr_rmmie_rcv_tot_pwrlvl' => 'USR-RMMIE-Rcv-Tot-PwrLvl',
+  'calling_station_id' => 'Calling-Station-Id',
+  'tunnel_rate_limit_burst' => 'Tunnel_Rate_Limit_Burst',
+  'usr_device_connected_to' => 'USR-Device-Connected-To',
+  'login_lat_service' => 'Login-LAT-Service',
+  'x_ascend_home_network_na' => 'X-Ascend-Home-Network-Name',
+  'ascend_h323_fegw_address' => 'Ascend-H323-Fegw-Address',
+  'usr_called_party_number' => 'USR-Called-Party-Number',
+  'ascend_remove_seconds' => 'Ascend-Remove-Seconds',
+  'shiva_user_attributes' => 'Shiva-User-Attributes',
+  'x_ascend_route_ipx' => 'X-Ascend-Route-IPX',
+  'acc_route_policy' => 'Acc-Route-Policy',
+  'x_ascend_client_gateway' => 'X-Ascend-Client-Gateway',
+  'ms_mppe_encryption_polic' => 'MS-MPPE-Encryption-Policy',
+  'x_ascend_data_filter' => 'X-Ascend-Data-Filter',
+  'ascend_atm_direct' => 'Ascend-ATM-Direct',
+  'ascend_session_type' => 'Ascend-Session-Type',
+  'x_ascend_fr_linkup' => 'X-Ascend-FR-LinkUp',
+  'ascend_metric' => 'Ascend-Metric',
+  'usr_speed_of_connection' => 'USR-Speed-Of-Connection',
+  'le_nat_outsource_outmap' => 'LE-NAT-Outsource-Outmap',
+  'pppoe_url' => 'PPPOE_URL',
+  'acct_mcast_out_octets' => 'Acct_Mcast_Out_Octets',
+  'ascend_callback' => 'Ascend-Callback',
+  'tunnel_client_auth_id' => 'Tunnel-Client-Auth-Id',
+  'acct_unique_session_id' => 'Acct-Unique-Session-Id',
+  'usr_port_tap_format' => 'USR-Port-Tap-Format',
+  'ascend_ckt_type' => 'Ascend-Ckt-Type',
+  'ascend_ppp_async_map' => 'Ascend-PPP-Async-Map',
+  'usr_acct_reason_code' => 'USR-Acct-Reason-Code',
+  'ascend_filter' => 'Ascend-Filter',
+  'h323_redirect_number' => 'h323-redirect-number',
+  'port_limit' => 'Port-Limit',
+  'x_ascend_shared_profile_' => 'X-Ascend-Shared-Profile-Enable',
+  'tunnel_police_rate' => 'Tunnel_Police_Rate',
+  'ascend_calling_id_screen' => 'Ascend-Calling-Id-Screening',
+  'usr_multicast_proxy' => 'USR-Multicast-Proxy',
+  'usr_bridging' => 'USR-Bridging',
+  'usr_originate_answer_mod' => 'USR-Originate-Answer-Mode',
+  'x_ascend_fr_dlci' => 'X-Ascend-FR-DLCI',
+  'usr_request_type' => 'USR-Request-Type',
+  'acc_dialout_auth_usernam' => 'Acc-Dialout-Auth-Username',
+  'ascend_host_info' => 'Ascend-Host-Info',
+  'usr_rmmie_num_of_updates' => 'USR-RMMIE-Num-Of-Updates',
+  'x_ascend_fr_profile_name' => 'X-Ascend-FR-Profile-Name',
+  'ascend_fr_direct_profile' => 'Ascend-FR-Direct-Profile',
+  'x_ascend_bridge' => 'X-Ascend-Bridge',
+  'tunnel_deadtime' => 'Tunnel_Deadtime',
+  'ms_chap_error' => 'MS-CHAP-Error',
+  'framed_route' => 'Framed-Route',
+  'expiration' => 'Expiration',
+  'ascend_backup' => 'Ascend-Backup',
+  'ascend_pre_output_octets' => 'Ascend-Pre-Output-Octets',
+  'framed_appletalk_zone' => 'Framed-AppleTalk-Zone',
+  'annex_audit_level' => 'Annex-Audit-Level',
+  'bind_auth_context' => 'Bind_Auth_Context',
+  'cisco_asing_ip_pool' => 'Cisco-Asing-IP-Pool',
+  'ascend_user_acct_base' => 'Ascend-User-Acct-Base',
+  'mcast_receive' => 'Mcast_Receive',
+  'usr_ds0' => 'USR-DS0',
+  'ms_ras_vendor' => 'MS-RAS-Vendor',
+  'tunnel_domain' => 'Tunnel_Domain',
+  'usr_secondary_nbns_serve' => 'USR-Secondary_NBNS_Server',
+  'tunnel_max_sessions' => 'Tunnel_Max_Sessions',
+  'ascend_ip_direct' => 'Ascend-IP-Direct',
+  'idle_timeout' => 'Idle-Timeout',
+  'tunnel_server_auth_id' => 'Tunnel-Server-Auth-Id',
+  'usr_start_time' => 'USR-Start-Time',
+  'usr_ip' => 'USR-IP',
+  'usr_gateway_ip_address' => 'USR-Gateway-IP-Address',
+  'usr_number_of_characters' => 'USR-Number-Of-Characters-Lost',
+  'ascend_dba_monitor' => 'Ascend-DBA-Monitor',
+  'ms_chap_domain' => 'MS-CHAP-Domain',
+  'cisco_pre_input_octets' => 'Cisco-Pre-Input-Octets',
+  'acct_session_time' => 'Acct-Session-Time',
+  'framed_ip_address' => 'Framed-IP-Address',
+  'x_ascend_ip_pool_definit' => 'X-Ascend-IP-Pool-Definition',
+  'erx_alternate_cli_access' => 'ERX-Alternate-Cli-Access-Level',
+  'medium_type' => 'Medium_Type',
+  'acct_output_octets_64' => 'Acct_Output_Octets_64',
+  'ascend_cir_timer' => 'Ascend-CIR-Timer',
+  'police_rate' => 'Police_Rate',
+  'ms_mppe_send_key' => 'MS-MPPE-Send-Key',
+  'ascend_multicast_gleave_' => 'Ascend-Multicast-GLeave-Delay',
+  'x_ascend_host_info' => 'X-Ascend-Host-Info',
+  'erx_egress_policy_name' => 'ERX-Egress-Policy-Name',
+  'user_name' => 'User-Name',
+  'bind_bypass_bypass' => 'Bind_Bypass_Bypass',
+  'annex_acct_servers' => 'Annex-Acct-Servers',
+  'usr_chassis_call_channel' => 'USR-Chassis-Call-Channel',
+  'annex_input_filter' => 'Annex-Input-Filter',
+  'ascend_home_agent_passwo' => 'Ascend-Home-Agent-Password',
+  'nas_port_type' => 'NAS-Port-Type',
+  'ascend_endpoint_disc' => 'Ascend-Endpoint-Disc',
+  'tunnel_police_burst' => 'Tunnel_Police_Burst',
+  'bind_auth_max_sessions' => 'Bind_Auth_Max_Sessions',
+  'x_ascend_fr_dce_n392' => 'X-Ascend-FR-DCE-N392',
+  'usr_connect_term_reason' => 'USR-Connect-Term-Reason',
+  'usr_mbi_ct_pri_card_span' => 'USR-Mbi_Ct_PRI_Card_Span_Line',
+  'erx_egress_statistics' => 'ERX-Egress-Statistics',
+  'ascend_fr_dte_n392' => 'Ascend-FR-DTE-N392',
+  'usr_esn' => 'USR-ESN',
+  'x_ascend_fr_dte_n392' => 'X-Ascend-FR-DTE-N392',
+  'x_ascend_fr_nailed_grp' => 'X-Ascend-FR-Nailed-Grp',
+  'ascend_bridge_non_pppoe' => 'Ascend-Bridge-Non-PPPoE',
+  'ascend_ipx_alias' => 'Ascend-IPX-Alias',
+  'acc_tunnel_port' => 'Acc-Tunnel-Port',
+  'acct_input_gigawords' => 'Acct-Input-Gigawords',
+  'ascend_maximum_channels' => 'Ascend-Maximum-Channels',
+  'x_ascend_ppp_async_map' => 'X-Ascend-PPP-Async-Map',
+  'usr_retrains_requested' => 'USR-Retrains-Requested',
+  'x_ascend_metric' => 'X-Ascend-Metric',
+  'acc_apsm_oversubscribed' => 'Acc-Apsm-Oversubscribed',
+  'erx_atm_pcr' => 'ERX-Atm-PCR',
+  'usr_ipx_routing' => 'USR-IPX-Routing',
+  'usr_tunneled_mlpp' => 'USR-Tunneled-MLPP',
+  'usr_send_script5' => 'USR-Send-Script5',
+  'ascend_traffic_shaper' => 'Ascend-Traffic-Shaper',
+  'ascend_bacp_enable' => 'Ascend-BACP-Enable',
+  'login_time' => 'Login-Time',
+  'ascend_call_type' => 'Ascend-Call-Type',
+  'erx_address_pool_name' => 'ERX-Address-Pool-Name',
+  'h323_incoming_conf_id' => 'h323-incoming-conf-id',
+  'packet_type' => 'Packet-Type',
+  'usr_security_resp_limit' => 'USR-Security-Resp-Limit',
+  'ip_address_pool_name' => 'Ip_Address_Pool_Name',
+  'ascend_cbcp_trunk_group' => 'Ascend-CBCP-Trunk-Group',
+  'ascend_ipx_node_addr' => 'Ascend-IPX-Node-Addr',
+  'ascend_menu_selector' => 'Ascend-Menu-Selector',
+  'usr_ds0s' => 'USR-DS0s',
+  'usr_actual_voltage' => 'USR-Actual-Voltage',
+  'annex_sw_version' => 'Annex-SW-Version',
+  'ascend_history_weigh_typ' => 'Ascend-History-Weigh-Type',
+  'ascend_receive_secret' => 'Ascend-Receive-Secret',
+  'usr_ip_rip_policies' => 'USR-IP-RIP-Policies',
+  'ascend_pw_warntime' => 'Ascend-PW-Warntime',
+  'x_ascend_assign_ip_serve' => 'X-Ascend-Assign-IP-Server',
+  'tunnel_session_auth_serv' => 'Tunnel_Session_Auth_Service_Grp',
+  'usr_blocks_resent' => 'USR-Blocks-Resent',
+  'usr_fallback_enabled' => 'USR-Fallback-Enabled',
+  'arap_challenge_response' => 'ARAP-Challenge-Response',
+  'tunnel_session_auth' => 'Tunnel_Session_Auth',
+  'usr_sync_async_mode' => 'USR-Sync-Async-Mode',
+  'client_port_dnis' => 'Client-Port-DNIS',
+  'ascend_ppp_vj_1172' => 'Ascend-PPP-VJ-1172',
+  'ascend_remote_addr' => 'Ascend-Remote-Addr',
+  'ascend_fr_n391' => 'Ascend-FR-N391',
+  'client_port_id' => 'Client-Port-Id',
+  'usr_num_fax_pages_proces' => 'USR-Num-Fax-Pages-Processed',
+  'le_ipsec_active_profile' => 'LE-IPSec-Active-Profile',
+  'usr_port_tap_facility' => 'USR-Port-Tap-Facility',
+  'usr_callback_type' => 'USR-Callback-Type',
+  'login_lat_group' => 'Login-LAT-Group',
+  'x_ascend_call_type' => 'X-Ascend-Call-Type',
+  'ascend_route_ip' => 'Ascend-Route-IP',
+  'usr_pw_vpn_id' => 'USR-PW_VPN_ID',
+  'le_nat_sess_dir_fail_act' => 'LE-NAT-Sess-Dir-Fail-Action',
+  'cisco_pre_output_octets' => 'Cisco-Pre-Output-Octets',
+  'h323_billing_model' => 'h323-billing-model',
+  'usr_equalization_type' => 'USR-Equalization-Type',
+  'acc_clearing_cause' => 'Acc-Clearing-Cause',
+  'x_ascend_menu_selector' => 'X-Ascend-Menu-Selector',
+  'x_ascend_netware_timeout' => 'X-Ascend-Netware-timeout',
+  'ascend_fr_linkup' => 'Ascend-FR-LinkUp',
+  'police_burst' => 'Police_Burst',
+  'ascend_filter_required' => 'Ascend-Filter-Required',
+  'usr_compression_algorith' => 'USR-Compression-Algorithm',
+  'le_ipsec_outsource_profi' => 'LE-IPSec-Outsource-Profile',
+  'x_ascend_idle_limit' => 'X-Ascend-Idle-Limit',
+  'usr_call_terminate_in_gm' => 'USR-Call-Terminate-in-GMT',
+  'usr_ipx_call_output_filt' => 'USR-IPX-Call-Output-Filter',
+  'ip_tos_field' => 'IP_TOS_Field',
+  'ascend_ip_tos_apply_to' => 'Ascend-IP-TOS-Apply-To',
+  'tunnel_l2f_second_passwo' => 'Tunnel_L2F_Second_Password',
+  'usr_call_event_code' => 'USR-Call-Event-Code',
+  'usr_rmmie_product_code' => 'USR-RMMIE-Product-Code',
+  'usr_host_type' => 'USR-Host-Type',
+  'ascend_send_auth' => 'Ascend-Send-Auth',
+  'shiva_compression_type' => 'Shiva-Compression-Type',
+  'filter_id' => 'Filter-Id',
+  'ascend_ft1_caller' => 'Ascend-FT1-Caller',
+  'erx_cli_initial_access_l' => 'ERX-Cli-Initial-Access-Level',
+  'usr_log_filter_packets' => 'USR-Log-Filter-Packets',
+  'ascend_fr_nailed_grp' => 'Ascend-FR-Nailed-Grp',
+  'usr_initial_tx_link_data' => 'USR-Initial-Tx-Link-Data-Rate',
+  'acc_input_errors' => 'Acc-Input-Errors',
+  'x_ascend_user_acct_port' => 'X-Ascend-User-Acct-Port',
+  'erx_secondary_wins' => 'ERX-Secondary-Wins',
+  'usr_rmmie_serial_number' => 'USR-RMMIE-Serial-Number',
+  'ascend_client_primary_dn' => 'Ascend-Client-Primary-DNS',
+  'usr_slot_connected_to' => 'USR-Slot-Connected-To',
+  'shiva_disconnect_reason' => 'Shiva-Disconnect-Reason',
+  'usr_receive_acc_map' => 'USR-Receive-Acc-Map',
+  'usr_compression_reset_mo' => 'USR-Compression-Reset-Mode',
+  'usr_rmmie_planned_discon' => 'USR-RMMIE-Planned-Disconnect',
+  'ascend_client_assign_dns' => 'Ascend-Client-Assign-DNS',
+  'ascend_fr_type' => 'Ascend-FR-Type',
+  'tunnel_client_endpoint' => 'Tunnel-Client-Endpoint',
+  'x_ascend_send_secret' => 'X-Ascend-Send-Secret',
+  'x_ascend_call_filter' => 'X-Ascend-Call-Filter',
+  'usr_ipx_rip_input_filter' => 'USR-IPX-RIP-Input-Filter',
+  'x_ascend_maximum_time' => 'X-Ascend-Maximum-Time',
+  'ascend_x25_pad_x3_profil' => 'Ascend-X25-Pad-X3-Profile',
+  'pvc_profile_name' => 'PVC_Profile_Name',
+  'ascend_global_call_id' => 'Ascend-Global-Call-Id',
+  'tunnel_local_name' => 'Tunnel_Local_Name',
+  'ascend_fr_t392' => 'Ascend-FR-T392',
+  'usr_dnis_reauthenticatio' => 'USR-DNIS-ReAuthentication',
+  'ascend_pre_output_packet' => 'Ascend-Pre-Output-Packets',
+  'ascend_token_immediate' => 'Ascend-Token-Immediate',
+  'usr_chassis_call_slot' => 'USR-Chassis-Call-Slot',
+  'rate_limit_burst' => 'Rate_Limit_Burst',
+  'cisco_route_ip' => 'Cisco-Route-IP',
+  'dhcp_max_leases' => 'DHCP_Max_Leases',
+  'user_category' => 'User-Category',
+  'x_ascend_maximum_call_du' => 'X-Ascend-Maximum-Call-Duration',
+  'bind_type' => 'Bind_Type',
+  'usr_framed_ipx_route' => 'USR-Framed-IPX-Route',
+  'rate_limit_rate' => 'Rate_Limit_Rate',
+  'ascend_atm_connect_vpi' => 'Ascend-ATM-Connect-Vpi',
+  'x_ascend_inc_channel_cou' => 'X-Ascend-Inc-Channel-Count',
+  'connect_info' => 'Connect-Info',
+  'x_ascend_pre_input_packe' => 'X-Ascend-Pre-Input-Packets',
+  'usr_port_tap_address' => 'USR-Port-Tap-Address',
+  'ascend_home_agent_udp_po' => 'Ascend-Home-Agent-UDP-Port',
+  'usr_final_rx_link_data_r' => 'USR-Final-Rx-Link-Data-Rate',
+  'usr_pw_usr_ifilter_ip' => 'USR-PW_USR_IFilter_IP',
+  'ascend_route_appletalk' => 'Ascend-Route-Appletalk',
+  'ms_chap_lm_enc_pw' => 'MS-CHAP-LM-Enc-PW',
+  'ascend_callback_delay' => 'Ascend-Callback-Delay',
+  'x_ascend_bacp_enable' => 'X-Ascend-BACP-Enable',
+  'bg_trans_bpdu' => 'BG_Trans_BPDU',
+  'huntgroup_name' => 'Huntgroup-Name',
+  'x_ascend_ipx_alias' => 'X-Ascend-IPX-Alias',
+  'x_ascend_secondary_home_' => 'X-Ascend-Secondary-Home-Agent',
+  'usr_ipx_wan' => 'USR-IPX-WAN',
+  'menu' => 'Menu',
+  'x_ascend_fr_direct_dlci' => 'X-Ascend-FR-Direct-DLCI',
+  'acct_status_type' => 'Acct-Status-Type',
+  'ascend_port_redir_server' => 'Ascend-Port-Redir-Server',
+  'acc_dns_server_sec' => 'Acc-Dns-Server-Sec',
+  'ascend_minimum_channels' => 'Ascend-Minimum-Channels',
+  'ascend_telnet_profile' => 'Ascend-Telnet-Profile',
+  'ascend_ipx_route' => 'Ascend-IPX-Route',
+  'usr_call_connect_in_gmt' => 'USR-Call-Connect-in-GMT',
+  'x_ascend_dba_monitor' => 'X-Ascend-DBA-Monitor',
+  'usr_event_id' => 'USR-Event-Id',
+  'ascend_inc_channel_count' => 'Ascend-Inc-Channel-Count',
+  'usr_send_script3' => 'USR-Send-Script3',
+  'framed_callback_id' => 'Framed-Callback-Id',
+  'arap_zone_access' => 'ARAP-Zone-Access',
+  'service_type' => 'Service-Type',
+  'usr_nfas_id' => 'USR-NFAS-ID',
+  'shiva_calling_number' => 'Shiva-Calling-Number',
+  'ascend_user_acct_host' => 'Ascend-User-Acct-Host',
+  'ascend_fr_link_mgt' => 'Ascend-FR-Link-Mgt',
+  'ms_primary_nbns_server' => 'MS-Primary-NBNS-Server',
+  'quintum_avpair' => 'Quintum-AVPair',
+  'x_ascend_home_agent_pass' => 'X-Ascend-Home-Agent-Password',
+  'ascend_transit_number' => 'Ascend-Transit-Number',
+  'ascend_cache_refresh' => 'Ascend-Cache-Refresh',
+  'versanet_termination_cau' => 'Versanet-Termination-Cause',
+  'ascend_user_acct_type' => 'Ascend-User-Acct-Type',
+  'usr_mic' => 'USR-MIC',
+  'ascend_base_channel_coun' => 'Ascend-Base-Channel-Count',
+  'x_ascend_dhcp_pool_numbe' => 'X-Ascend-DHCP-Pool-Number',
+  'ms_chap2_success' => 'MS-CHAP2-Success',
+  'cisco_idle_limit' => 'Cisco-Idle-Limit',
+  'ascend_pw_lifetime' => 'Ascend-PW-Lifetime',
+  'usr_packet_bus_session' => 'USR-Packet-Bus-Session',
+  'ascend_atm_loopback_cell' => 'Ascend-ATM-Loopback-Cell-Loss',
+  'acct_input_packets_64' => 'Acct_Input_Packets_64',
+  'ascend_modem_slotno' => 'Ascend-Modem-SlotNo',
+  'usr_characters_received' => 'USR-Characters-Received',
+  'ms_bap_usage' => 'MS-BAP-Usage',
+  'cisco_data_filter' => 'Cisco-Data-Filter',
+  'ascend_seconds_of_histor' => 'Ascend-Seconds-Of-History',
+  'h323_setup_time' => 'h323-setup-time',
+  'acc_dialout_auth_passwor' => 'Acc-Dialout-Auth-Password',
+  'le_nat_outsource_inmap' => 'LE-NAT-Outsource-Inmap',
+  'usr_sap_filter_in' => 'USR-SAP-Filter-In',
+  'framed_appletalk_link' => 'Framed-AppleTalk-Link',
+  'usr_initial_rx_link_data' => 'USR-Initial-Rx-Link-Data-Rate',
+  'usr_ospf_addressless_ind' => 'USR-OSPF-Addressless-Index',
+  'usr_ipx' => 'USR-IPX',
+  'shiva_connect_reason' => 'Shiva-Connect-Reason',
+  'cisco_ppp_vj_slot_comp' => 'Cisco-PPP-VJ-Slot-Comp',
+  'ascend_atm_vpi' => 'Ascend-ATM-Vpi',
+  'acc_ml_mlx_admin_state' => 'Acc-ML-MLX-Admin-State',
+  'usr_igmp_robustness' => 'USR-IGMP-Robustness',
+  'add_prefix' => 'Add-Prefix',
+  'x_ascend_call_by_call' => 'X-Ascend-Call-By-Call',
+  'x_ascend_connect_progres' => 'X-Ascend-Connect-Progress',
+  'usr_at_rtmp_input_filter' => 'USR-AT-RTMP-Input-Filter',
+  'erx_igmp_enable' => 'ERX-Igmp-Enable',
+  'usr_rmmie_rcv_pwrlvl_375' => 'USR-RMMIE-Rcv-PwrLvl-3750Hz',
+  'usr_pw_packet' => 'USR-PW_Packet',
+  'dialback_no' => 'Dialback-No',
+  'ascend_ip_tos_precedence' => 'Ascend-IP-TOS-Precedence',
+  'annex_cli_filter' => 'Annex-CLI-Filter',
+  'x_ascend_dial_number' => 'X-Ascend-Dial-Number',
+  'usr_iwf_call_identifier' => 'USR-IWF-Call-Identifier',
+  'ms_secondary_dns_server' => 'MS-Secondary-DNS-Server',
+  'ascend_client_secondary_' => 'Ascend-Client-Secondary-WINS',
+  'shiva_type_of_service' => 'Shiva-Type-Of-Service',
+  'usr_framed_ip_address_po' => 'USR-Framed_IP_Address_Pool_Name',
+  'bind_ses_context' => 'Bind_Ses_Context',
+  'acc_reason_code' => 'Acc-Reason-Code',
+  'ms_chap_cpw_1' => 'MS-CHAP-CPW-1',
+  'h323_call_type' => 'h323-call-type',
+  'ascend_fr_08_mode' => 'Ascend-FR-08-Mode',
+  'usr_calling_party_number' => 'USR-Calling-Party-Number',
+  'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-RtLim',
+  'usr_reply_script2' => 'USR-Reply-Script2',
+  'usr_security_login_limit' => 'USR-Security-Login-Limit',
+  'cisco_link_compression' => 'Cisco-Link-Compression',
+  'usr_et_bridge_output_fil' => 'USR-ET-Bridge-Output-Filter',
+  'ascend_vrouter_name' => 'Ascend-VRouter-Name',
+  'usr_modem_setup_time' => 'USR-Modem-Setup-Time',
+  'cisco_ip_direct' => 'Cisco-IP-Direct',
+  'x_ascend_temporary_rtes' => 'X-Ascend-Temporary-Rtes',
+  'ascend_x25_pad_alias_3' => 'Ascend-X25-Pad-Alias-3',
+  'usr_rmmie_pwrlvl_xmit_lv' => 'USR-RMMIE-PwrLvl-Xmit-Lvl',
+  'configuration_token' => 'Configuration-Token',
+  'usr_at_rtmp_output_filte' => 'USR-AT-RTMP-Output-Filter',
+  'usr_ip_default_route_opt' => 'USR-IP-Default-Route-Option',
+  'ascend_calling_subaddres' => 'Ascend-Calling-Subaddress',
+  'stripped_user_name' => 'Stripped-User-Name',
+  'cisco_call_filter' => 'Cisco-Call-Filter',
+  'termination_menu' => 'Termination-Menu',
+  'port_message' => 'Port-Message',
+  'usr_igmp_maximum_respons' => 'USR-IGMP-Maximum-Response-Time',
+  'erx_ingress_policy_name' => 'ERX-Ingress-Policy-Name',
+  'ascend_call_attempt_limi' => 'Ascend-Call-Attempt-Limit',
+  'acc_service_profile' => 'Acc-Service-Profile',
+  'ascend_bir_proxy' => 'Ascend-BIR-Proxy',
+  'ascend_x25_nui_prompt' => 'Ascend-X25-Nui-Prompt',
+  'usr_rmmie_pwrlvl_noise_l' => 'USR-RMMIE-PwrLvl-Noise-Lvl',
+  'usr_rmmie_pwrlvl_nearech' => 'USR-RMMIE-PwrLvl-NearEcho-Canc',
+  'x_ascend_multicast_clien' => 'X-Ascend-Multicast-Client',
+  'usr_unauthenticated_time' => 'USR-Unauthenticated-Time',
+  'acc_callback_cbcp_type' => 'Acc-Callback-CBCP-Type',
+  'login_service' => 'Login-Service',
+  'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Bound',
+  'ascend_dial_number' => 'Ascend-Dial-Number',
+  'x_ascend_remote_addr' => 'X-Ascend-Remote-Addr',
+  'usr_rmmie_rcv_pwrlvl_330' => 'USR-RMMIE-Rcv-PwrLvl-3300Hz',
+  'usr_call_end_date_time' => 'USR-Call-End-Date-Time',
+  'bind_dot1q_slot' => 'Bind_Dot1q_Slot',
+  'le_connect_detail' => 'LE-Connect-Detail',
+  'annex_user_level' => 'Annex-User-Level',
+  'tunnel_dnis' => 'Tunnel_DNIS',
+  'assigned_ip_address' => 'Assigned_IP_Address',
+  'acc_bridging_support' => 'Acc-Bridging-Support',
+  'usr_channel' => 'USR-Channel',
+  'arap_security_data' => 'ARAP-Security-Data',
+  'bind_auth_service_grp' => 'Bind_Auth_Service_Grp',
+  'x_ascend_pre_output_pack' => 'X-Ascend-Pre-Output-Packets',
+  'x_ascend_seconds_of_hist' => 'X-Ascend-Seconds-Of-History',
+  'h323_voice_quality' => 'h323-voice-quality',
+  'usr_rmmie_last_update_ti' => 'USR-RMMIE-Last-Update-Time',
+  'usr_disconnect_cause_ind' => 'USR-Disconnect-Cause-Indicator',
+  'usr_channel_connected_to' => 'USR-Channel-Connected-To',
+  'ascend_calling_id_number' => 'Ascend-Calling-Id-Number-Plan',
+  'usr_number_of_fallbacks' => 'USR-Number-of-Fallbacks',
+  'usr_ip_call_output_filte' => 'USR-IP-Call-Output-Filter',
+  'ascend_bir_enable' => 'Ascend-BIR-Enable',
+  'usr_connect_time_limit' => 'USR-Connect-Time-Limit',
+  'ascend_presession_time' => 'Ascend-PreSession-Time',
+  'ascend_private_route_req' => 'Ascend-Private-Route-Required',
+  'ascend_dsl_cir_xmit_limi' => 'Ascend-Dsl-CIR-Xmit-Limit',
+  'framed_compression' => 'Framed-Compression',
+  'ascend_svc_enabled' => 'Ascend-SVC-Enabled',
+  'proxy_state' => 'Proxy-State',
+  'ascend_tunnel_vrouter_na' => 'Ascend-Tunnel-VRouter-Name',
+  'usr_ipx_call_input_filte' => 'USR-IPX-Call-Input-Filter',
+  'x_ascend_assign_ip_globa' => 'X-Ascend-Assign-IP-Global-Pool',
+  'erx_alternate_cli_vroute' => 'ERX-Alternate-Cli-Vrouter-Name',
+  'ascend_dhcp_maximum_leas' => 'Ascend-DHCP-Maximum-Leases',
+  'ascend_modem_shelfno' => 'Ascend-Modem-ShelfNo',
+  'bind_auth_protocol' => 'Bind_Auth_Protocol',
+  'shasta_user_privilege' => 'Shasta-User-Privilege',
+  'acct_interim_interval' => 'Acct-Interim-Interval',
+  'hint' => 'Hint',
+  'x_ascend_target_util' => 'X-Ascend-Target-Util',
+  'ms_link_drop_time_limit' => 'MS-Link-Drop-Time-Limit',
+  'acc_access_partition' => 'Acc-Access-Partition',
+  'x_ascend_multilink_id' => 'X-Ascend-Multilink-ID',
+  'usr_power_supply_number' => 'USR-Power-Supply-Number',
+  'acc_ipx_compression' => 'Acc-Ipx-Compression',
+  'nomadix_bw_down' => 'Nomadix-Bw-Down',
+  'usr_call_reference_numbe' => 'USR-Call-Reference-Number',
+  'cisco_target_util' => 'Cisco-Target-Util',
+  'usr_back_channel_data_ra' => 'USR-Back-Channel-Data-Rate',
+  'acc_ip_gateway_sec' => 'Acc-Ip-Gateway-Sec',
+  'usr_dte_ring_no_answer_l' => 'USR-DTE-Ring-No-Answer-Limit',
+  'usr_connect_time' => 'USR-Connect-Time',
+  'ascend_ip_pool_definitio' => 'Ascend-IP-Pool-Definition',
+  'usr_call_start_date_time' => 'USR-Call-Start-Date-Time',
+  'dialback_name' => 'Dialback-Name',
+  'bind_tun_context' => 'Bind_Tun_Context',
+  'h323_redirect_ip_address' => 'h323-redirect-ip-address',
+  'annex_keypress_timeout' => 'Annex-Keypress-Timeout',
+  'ascend_x25_pad_alias_1' => 'Ascend-X25-Pad-Alias-1',
+  'ms_chap_response' => 'MS-CHAP-Response',
+  'usr_max_channels' => 'USR-Max-Channels',
+  'ascend_fr_dte_n393' => 'Ascend-FR-DTE-N393',
+  'ascend_pre_input_octets' => 'Ascend-Pre-Input-Octets',
+  'erx_atm_mbs' => 'ERX-Atm-MBS',
+  'usr_line_reversals' => 'USR-Line-Reversals',
+  'x_ascend_third_prompt' => 'X-Ascend-Third-Prompt',
+  'x_ascend_pw_warntime' => 'X-Ascend-PW-Warntime',
+  'ascend_data_filter' => 'Ascend-Data-Filter',
+  'framed_address' => 'Framed-Address',
+  'context_name' => 'Context-Name',
+  'usr_send_script2' => 'USR-Send-Script2',
+  'ms_arap_pw_change_reason' => 'MS-ARAP-PW-Change-Reason',
+  'acct_session_id' => 'Acct-Session-Id',
+  'initial_modulation_type' => 'Initial-Modulation-Type',
+  'ascend_h323_gatekeeper' => 'Ascend-H323-Gatekeeper',
+  'x_ascend_fcp_parameter' => 'X-Ascend-FCP-Parameter',
+  'tunnel_type' => 'Tunnel-Type',
+  'multi_link_flag' => 'Multi-Link-Flag',
+  'ascend_idle_limit' => 'Ascend-Idle-Limit',
+  'password_retry' => 'Password-Retry',
+  'h323_remote_address' => 'h323-remote-address',
+  'erx_atm_service_category' => 'ERX-Atm-Service-Category',
+  'acct_input_packets' => 'Acct-Input-Packets',
+  'h323_disconnect_time' => 'h323-disconnect-time',
+  'ascend_billing_number' => 'Ascend-Billing-Number',
+  'usr_syslog_tap' => 'USR-Syslog-Tap',
+  'ms_mppe_encryption_type' => 'MS-MPPE-Encryption-Type',
+  'ascend_assign_ip_pool' => 'Ascend-Assign-IP-Pool',
+  'usr_routing_protocol' => 'USR-Routing-Protocol',
+  'usr_rad_location_type' => 'USR-Rad-Location-Type',
+  'usr_characters_sent' => 'USR-Characters-Sent',
+  'usr_mp_edo_hiper' => 'USR-MP-EDO-HIPER',
+  'annex_host_restrict' => 'Annex-Host-Restrict',
+  'user_service_type' => 'User-Service-Type',
+  'acct_multi_session_id' => 'Acct-Multi-Session-Id',
+  'ms_chap_cpw_2' => 'MS-CHAP-CPW-2',
+  'x_ascend_primary_home_ag' => 'X-Ascend-Primary-Home-Agent',
+  'x_ascend_dialout_allowed' => 'X-Ascend-Dialout-Allowed',
+  'ascend_connect_progress' => 'Ascend-Connect-Progress',
+  'x_ascend_ara_pw' => 'X-Ascend-Ara-PW',
+  'ns_mta_md5_password' => 'NS-MTA-MD5-Password',
+  'callback_number' => 'Callback-Number',
+  'acct_output_packets_64' => 'Acct_Output_Packets_64',
+  'x_ascend_user_acct_key' => 'X-Ascend-User-Acct-Key',
+  'ascend_modem_portno' => 'Ascend-Modem-PortNo',
+  'ascend_assign_ip_server' => 'Ascend-Assign-IP-Server',
+  'ascend_fcp_parameter' => 'Ascend-FCP-Parameter',
+  'ascend_inter_arrival_jit' => 'Ascend-Inter-Arrival-Jitter',
+  'client_ip_address' => 'Client-IP-Address',
+  'usr_number_of_link_timeo' => 'USR-Number-of-Link-Timeouts',
+  'ascend_dsl_cir_recv_limi' => 'Ascend-Dsl-CIR-Recv-Limit',
+  'ms_acct_eap_type' => 'MS-Acct-EAP-Type',
+  'x_ascend_user_acct_type' => 'X-Ascend-User-Acct-Type',
+  'usr_rmmie_x2_status' => 'USR-RMMIE-x2-Status',
+  'ascend_dsl_downstream_li' => 'Ascend-Dsl-Downstream-Limit',
+  'shiva_customer_id' => 'Shiva-Customer-Id',
+  'lac_real_port' => 'LAC_Real_Port',
+  'h323_connect_time' => 'h323-connect-time',
+  'old_password' => 'Old-Password',
+  'usr_vpn_gw_location_id' => 'USR-VPN-GW-Location-Id',
+  'x_ascend_if_netmask' => 'X-Ascend-IF-Netmask',
+  'add_suffix' => 'Add-Suffix',
+  'x_ascend_client_assign_d' => 'X-Ascend-Client-Assign-DNS',
+  'usr_q931_call_reference_' => 'USR-Q931-Call-Reference-Value',
+  'usr_terminal_type' => 'USR-Terminal-Type',
+  'usr_spoofing' => 'USR-Spoofing',
+  'erx_tunnel_password' => 'ERX-Tunnel-Password',
+  'ascend_assign_ip_client' => 'Ascend-Assign-IP-Client',
+  'usr_server_time' => 'USR-Server-Time',
+  'ascend_data_svc' => 'Ascend-Data-Svc',
+  'annex_authen_servers' => 'Annex-Authen-Servers',
+  'nomadix_bw_up' => 'Nomadix-Bw-Up',
+  'shiva_link_speed' => 'Shiva-Link-Speed',
+  'usr_reply_script6' => 'USR-Reply-Script6',
+  'usr_expansion_algorithm' => 'USR-Expansion-Algorithm',
+  'x_ascend_mpp_idle_percen' => 'X-Ascend-MPP-Idle-Percent',
+  'cisco_data_rate' => 'Cisco-Data-Rate',
+  'usr_primary_dns_server' => 'USR-Primary_DNS_Server',
+  'erx_local_loopback_inter' => 'ERX-Local-Loopback-Interface',
+  'ascend_target_util' => 'Ascend-Target-Util',
+  'usr_default_dte_data_rat' => 'USR-Default-DTE-Data-Rate',
+  'x_ascend_event_type' => 'X-Ascend-Event-Type',
+  'usr_mp_mrru' => 'USR-MP-MRRU',
+  'bind_bypass_context' => 'Bind_Bypass_Context',
+  'no_such_attribute' => 'No-Such-Attribute',
+  'acct_mcast_out_packets' => 'Acct_Mcast_Out_Packets',
+  'tunnel_medium_type' => 'Tunnel-Medium-Type',
+  'acc_callback_delay' => 'Acc-Callback-Delay',
+  'x_ascend_home_agent_udp_' => 'X-Ascend-Home-Agent-UDP-Port',
+  'acct_input_octets_64' => 'Acct_Input_Octets_64',
+  'ascend_atm_connect_vci' => 'Ascend-ATM-Connect-Vci',
+  'erx_primary_dns' => 'ERX-Primary-Dns',
+  'ascend_xmit_rate' => 'Ascend-Xmit-Rate',
+  'ms_new_arap_password' => 'MS-New-ARAP-Password',
+  'usr_call_error_code' => 'USR-Call-Error-Code',
+  'acct_output_octets' => 'Acct-Output-Octets',
+  'usr_failure_to_connect_r' => 'USR-Failure-to-Connect-Reason',
+  'ascend_num_in_multilink' => 'Ascend-Num-In-Multilink',
+  'x_ascend_number_sessions' => 'X-Ascend-Number-Sessions',
+  'usr_ip_rip_output_filter' => 'USR-IP-RIP-Output-Filter',
+  'usr_chassis_temp_thresho' => 'USR-Chassis-Temp-Threshold',
+  'usr_blocks_sent' => 'USR-Blocks-Sent',
+  'usr_ids0_call_type' => 'USR-IDS0-Call-Type',
+  'acc_ccp_option' => 'Acc-Ccp-Option',
+  'ascend_client_gateway' => 'Ascend-Client-Gateway',
+  'x_ascend_multicast_rate_' => 'X-Ascend-Multicast-Rate-Limit',
+  'le_ipsec_passive_profile' => 'LE-IPSec-Passive-Profile',
+  'usr_chassis_call_span' => 'USR-Chassis-Call-Span',
+  'usr_mobileip_home_agent_' => 'USR-MobileIP-Home-Agent-Address',
+  'password' => 'Password',
+  'le_nat_log_options' => 'LE-NAT-Log-Options',
+  'x_ascend_ppp_address' => 'X-Ascend-PPP-Address',
+  'usr_fallback_limit' => 'USR-Fallback-Limit',
+  'suffix' => 'Suffix',
+  'usr_multicast_receive' => 'USR-Multicast-Receive',
+  'client_dns_sec' => 'Client_DNS_Sec',
+  'annex_product_name' => 'Annex-Product-Name',
+  'cisco_pw_lifetime' => 'Cisco-PW-Lifetime',
+  'x_ascend_fr_dce_n393' => 'X-Ascend-FR-DCE-N393',
+  'x_ascend_ts_idle_limit' => 'X-Ascend-TS-Idle-Limit',
+  'usr_last_number_dialed_o' => 'USR-Last-Number-Dialed-Out',
+  'mcast_send' => 'Mcast_Send',
+  'pppoe_motm' => 'PPPOE_MOTM',
+  'usr_pw_usr_ifilter_ipx' => 'USR-PW_USR_IFilter_IPX',
+  'usr_pw_tunnel_authentica' => 'USR-PW_Tunnel_Authentication',
+  'ascend_source_ip_check' => 'Ascend-Source-IP-Check',
+  'ascend_assign_ip_global_' => 'Ascend-Assign-IP-Global-Pool',
+  'ms_ras_version' => 'MS-RAS-Version',
+  'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Ttl',
+  'x_ascend_modem_slotno' => 'X-Ascend-Modem-SlotNo',
+  'acc_ml_call_threshold' => 'Acc-ML-Call-Threshold',
+  'ascend_menu_item' => 'Ascend-Menu-Item',
+  'usr_cdma_call_reference_' => 'USR-CDMA-Call-Reference-Number',
+  'callback_id' => 'Callback-Id',
+  'framed_ipx_network' => 'Framed-IPX-Network',
+  'x_ascend_disconnect_caus' => 'X-Ascend-Disconnect-Cause',
+  'ascend_user_acct_key' => 'Ascend-User-Acct-Key',
+  'x_ascend_pw_lifetime' => 'X-Ascend-PW-Lifetime',
+  'user_name_is_star' => 'User-Name-Is-Star',
+  'x_ascend_authen_alias' => 'X-Ascend-Authen-Alias',
+  'framed_pool' => 'Framed-Pool',
+  'ms_primary_dns_server' => 'MS-Primary-DNS-Server',
+  'realm' => 'Realm',
+  'arap_features' => 'ARAP-Features',
+  'acc_connect_tx_speed' => 'Acc-Connect-Tx-Speed',
+  'usr_last_number_dialed_i' => 'USR-Last-Number-Dialed-In-DNIS',
+  'usr_chassis_temperature' => 'USR-Chassis-Temperature',
+  'x_ascend_xmit_rate' => 'X-Ascend-Xmit-Rate',
+  'x_ascend_send_passwd' => 'X-Ascend-Send-Passwd',
+  'le_modem_info' => 'LE-Modem-Info',
+  'ascend_ipx_peer_mode' => 'Ascend-IPX-Peer-Mode',
+  'le_nat_other_session_tim' => 'LE-NAT-Other-Session-Timeout',
+  'tunnel_rate_limit_rate' => 'Tunnel_Rate_Limit_Rate',
+  'ascend_maximum_call_dura' => 'Ascend-Maximum-Call-Duration',
+  'ascend_dhcp_pool_number' => 'Ascend-DHCP-Pool-Number',
+  'x_ascend_callback' => 'X-Ascend-Callback',
+  'ascend_access_intercept_' => 'Ascend-Access-Intercept-Log',
+  'usr_iwf_ip_address' => 'USR-IWF-IP-Address',
+  'nas_port_id' => 'NAS-Port-Id',
+  'le_advice_of_charge' => 'LE-Advice-of-Charge',
+  'ascend_add_seconds' => 'Ascend-Add-Seconds',
+  'annex_transmit_speed' => 'Annex-Transmit-Speed',
+  'usr_port_tap' => 'USR-Port-Tap',
+  'usr_at_call_input_filter' => 'USR-AT-Call-Input-Filter',
+  'ascend_qos_downstream' => 'Ascend-QOS-Downstream',
+  'ascend_x25_reverse_charg' => 'Ascend-X25-Reverse-Charging',
+  'lac_port' => 'LAC_Port',
+  'tunnel_assignment_id' => 'Tunnel-Assignment-Id',
+  'fall_through' => 'Fall-Through',
+  'cisco_disconnect_cause' => 'Cisco-Disconnect-Cause',
+  'module_message' => 'Module-Message',
+  'framed_ip_netmask' => 'Framed-IP-Netmask',
+  'ascend_egress_enabled' => 'Ascend-Egress-Enabled',
+  'ascend_dsl_rate_mode' => 'Ascend-Dsl-Rate-Mode',
+  'x_ascend_client_primary_' => 'X-Ascend-Client-Primary-DNS',
+  'usr_pw_usr_ofilter_sap' => 'USR-PW_USR_OFilter_SAP',
+  'acct_terminate_cause' => 'Acct-Terminate-Cause',
+  'x_ascend_fr_dte_n393' => 'X-Ascend-FR-DTE-N393',
+  'x_ascend_call_block_dura' => 'X-Ascend-Call-Block-Duration',
+  'ascend_ppp_address' => 'Ascend-PPP-Address',
+  'caller_id' => 'Caller-ID',
+  'bind_int_interface_name' => 'Bind_Int_Interface_Name',
+  'x_ascend_ppp_vj_slot_com' => 'X-Ascend-PPP-VJ-Slot-Comp',
+  'usr_modem_group' => 'USR-Modem-Group',
+  'cisco_maximum_channels' => 'Cisco-Maximum-Channels',
+  'ascend_link_compression' => 'Ascend-Link-Compression',
+  'usr_retrains_granted' => 'USR-Retrains-Granted',
+  'ascend_dropped_packets' => 'Ascend-Dropped-Packets',
+  'usr_pw_usr_ofilter_ip' => 'USR-PW_USR_OFilter_IP',
+  'quintum_nas_port' => 'Quintum-NAS-Port',
+  'annex_tunnel_authen_mode' => 'Annex-Tunnel-Authen-Mode',
+  'tunnel_function' => 'Tunnel_Function',
+  'usr_mp_edo' => 'USR-MP-EDO',
+  'le_nat_outmap' => 'LE-NAT-Outmap',
+  'usr_modulation_type' => 'USR-Modulation-Type',
+  'ascend_maximum_time' => 'Ascend-Maximum-Time',
+  'annex_callback_portlist' => 'Annex-Callback-Portlist',
+  'x_ascend_remove_seconds' => 'X-Ascend-Remove-Seconds',
+  'tunnel_server_endpoint' => 'Tunnel-Server-Endpoint',
+  'arap_password' => 'ARAP-Password',
+  'ms_chap_mppe_keys' => 'MS-CHAP-MPPE-Keys',
+  'ascend_source_auth' => 'Ascend-Source-Auth',
+  'group' => 'Group',
+  'usr_send_script6' => 'USR-Send-Script6',
+  'le_nat_inmap' => 'LE-NAT-Inmap',
+  'chap_password' => 'CHAP-Password',
+  'annex_primary_nbns_serve' => 'Annex-Primary-NBNS-Server',
+  'annex_receive_speed' => 'Annex-Receive-Speed',
+  'usr_rmmie_manufacturer_i' => 'USR-RMMIE-Manufacturer-ID',
+  'bind_l2tp_flow_control' => 'Bind_L2TP_Flow_Control',
+  'smb_account_ctrl' => 'SMB-Account-CTRL',
+  'ascend_calling_id_presen' => 'Ascend-Calling-Id-Presentatn',
+  'ascend_ip_pool_chaining' => 'Ascend-IP-Pool-Chaining',
+  'le_admin_group' => 'LE-Admin-Group',
+  'nas_identifier' => 'NAS-Identifier',
+  'x_ascend_history_weigh_t' => 'X-Ascend-History-Weigh-Type',
+  'tunnel_connection_id' => 'Tunnel-Connection-Id',
+  'nas_real_port' => 'NAS_Real_Port',
+  'ms_old_arap_password' => 'MS-Old-ARAP-Password',
+  'usr_ip_rip_simple_auth_p' => 'USR-IP-RIP-Simple-Auth-Password',
+  'erx_primary_wins' => 'ERX-Primary-Wins',
+  'usr_pw_index' => 'USR-PW_Index',
+  'erx_cli_allow_all_vr_acc' => 'ERX-Cli-Allow-All-VR-Access',
+  'le_ipsec_log_options' => 'LE-IPSec-Log-Options',
+  'ascend_home_agent_ip_add' => 'Ascend-Home-Agent-IP-Addr',
+  'annex_re_chap_timeout' => 'Annex-Re-CHAP-Timeout',
+  'usr_final_tx_link_data_r' => 'USR-Final-Tx-Link-Data-Rate',
+  'client_dns_pri' => 'Client_DNS_Pri',
+  'usr_primary_nbns_server' => 'USR-Primary_NBNS_Server',
+  'usr_cusr_hat_script_rule' => 'USR-CUSR-hat-Script-Rules',
+  'ascend_multicast_rate_li' => 'Ascend-Multicast-Rate-Limit',
+  'usr_rmmie_pwrlvl_farecho' => 'USR-RMMIE-PwrLvl-FarEcho-Canc',
+  'acc_acct_on_off_reason' => 'Acc-Acct-On-Off-Reason',
+  'le_multicast_client' => 'LE-Multicast-Client',
+  'ascend_send_passwd' => 'Ascend-Send-Passwd',
+  'annex_unauthenticated_ti' => 'Annex-Unauthenticated-Time',
+  'tunnel_context' => 'Tunnel_Context',
+  'acc_nbns_server_sec' => 'Acc-Nbns-Server-Sec',
+  'usr_channel_decrement' => 'USR-Channel-Decrement',
+  'usr_rmmie_firmware_versi' => 'USR-RMMIE-Firmware-Version',
+  'ms_chap_challenge' => 'MS-CHAP-Challenge',
+  'x_ascend_client_secondar' => 'X-Ascend-Client-Secondary-DNS',
+  'ascend_cbcp_mode' => 'Ascend-CBCP-Mode',
+  'ascend_x25_rpoa' => 'Ascend-X25-Rpoa',
+  'usr_dtr_false_timeout' => 'USR-DTR-False-Timeout',
+  'usr_rad_multicast_routin' => 'USR-Rad-Multicast-Routing-Proto',
+  'ascend_x25_pad_x3_parame' => 'Ascend-X25-Pad-X3-Parameters',
+  'usr_physical_state' => 'USR-Physical-State',
+  'ascend_fr_t391' => 'Ascend-FR-T391',
+  'bind_dot1q_port' => 'Bind_Dot1q_Port',
+  'lac_port_type' => 'LAC_Port_Type',
+  'bg_aging_time' => 'BG_Aging_Time',
+  'erx_atm_scr' => 'ERX-Atm-SCR',
+  'x_ascend_menu_item' => 'X-Ascend-Menu-Item',
+  'ascend_x25_pad_banner' => 'Ascend-X25-Pad-Banner',
+  'h323_gw_id' => 'h323-gw-id',
+  'h323_preferred_lang' => 'h323-preferred-lang',
+  'usr_min_compression_size' => 'USR-Min-Compression-Size',
+  'usr_compression_type' => 'USR-Compression-Type',
+  'x_ascend_call_attempt_li' => 'X-Ascend-Call-Attempt-Limit',
+  'ascend_dialout_allowed' => 'Ascend-Dialout-Allowed',
+  'annex_local_username' => 'Annex-Local-Username',
+  'cisco_pre_input_packets' => 'Cisco-Pre-Input-Packets',
+  'ascend_send_secret' => 'Ascend-Send-Secret',
+  'shiva_function' => 'Shiva-Function',
+  'usr_dte_data_idle_timout' => 'USR-DTE-Data-Idle-Timout',
+  'usr_number_of_blers' => 'USR-Number-of-Blers',
+  'usr_card_type' => 'USR-Card-Type',
+  'ascend_token_idle' => 'Ascend-Token-Idle',
+  'x_ascend_group' => 'X-Ascend-Group',
+  'nt_password' => 'NT-Password',
+  'acct_mcast_in_packets' => 'Acct_Mcast_In_Packets',
+  'usr_supports_tags' => 'USR-Supports-Tags',
+  'ascend_number_sessions' => 'Ascend-Number-Sessions',
+  'x_ascend_add_seconds' => 'X-Ascend-Add-Seconds',
+  'usr_number_of_upshifts' => 'USR-Number-of-Upshifts',
+  'proxy_to_realm' => 'Proxy-To-Realm',
+  'acc_callback_num_valid' => 'Acc-Callback-Num-Valid',
+  'x_ascend_maximum_channel' => 'X-Ascend-Maximum-Channels',
+  'acc_access_community' => 'Acc-Access-Community',
+  'x_ascend_fr_direct_profi' => 'X-Ascend-FR-Direct-Profile',
+  'usr_send_name' => 'USR-Send-Name',
+  'usr_chassis_slot' => 'USR-Chassis-Slot',
+  'login_ip_host' => 'Login-IP-Host',
+  'ascend_netware_timeout' => 'Ascend-Netware-timeout',
+  'vendor_specific' => 'Vendor-Specific',
+  'bind_sub_user_at_context' => 'Bind_Sub_User_At_Context',
+  'ascend_fr_direct_dlci' => 'Ascend-FR-Direct-DLCI',
+  'ascend_atm_fault_managem' => 'Ascend-ATM-Fault-Management',
+  'ascend_qos_upstream' => 'Ascend-QOS-Upstream',
+  'source_validation' => 'Source_Validation',
+  'x_ascend_token_expiry' => 'X-Ascend-Token-Expiry',
+  'ascend_dec_channel_count' => 'Ascend-Dec-Channel-Count',
+  'usr_local_framed_ip_addr' => 'USR-Local-Framed-IP-Addr',
+  'usr_service_option' => 'USR-Service-Option',
+  'usr_transmit_acc_map' => 'USR-Transmit-Acc-Map',
+  'ascend_fr_direct' => 'Ascend-FR-Direct',
+  'x_ascend_expect_callback' => 'X-Ascend-Expect-Callback',
+  'acc_ml_damping_factor' => 'Acc-ML-Damping-Factor',
+  'framed_netmask' => 'Framed-Netmask',
+  'usr_connect_speed' => 'USR-Connect-Speed',
+  'ascend_client_primary_wi' => 'Ascend-Client-Primary-WINS',
+  'cisco_multilink_id' => 'Cisco-Multilink-ID',
+  'bg_span_dis' => 'BG_Span_Dis',
+  'ascend_multilink_id' => 'Ascend-Multilink-ID',
+  'tunnel_max_tunnels' => 'Tunnel_Max_Tunnels',
+  'ascend_dhcp_reply' => 'Ascend-DHCP-Reply',
+  'ascend_x25_cug' => 'Ascend-X25-Cug',
+  'shiva_network_protocols' => 'Shiva-Network-Protocols',
+  'ascend_ara_pw' => 'Ascend-Ara-PW',
+  'ip_host_addr' => 'Ip_Host_Addr',
+  'le_ip_gateway' => 'LE-IP-Gateway',
+  'usr_mobile_numbytes_txed' => 'USR-Mobile-NumBytes-Txed',
+  'x_ascend_fr_t392' => 'X-Ascend-FR-T392',
+  'cisco_pre_output_packets' => 'Cisco-Pre-Output-Packets',
+  'tunnel_group' => 'Tunnel_Group',
+  'bind_sub_password' => 'Bind_Sub_Password',
+  'eap_message' => 'EAP-Message',
+  'exec_program' => 'Exec-Program',
+  'bg_path_cost' => 'BG_Path_Cost',
+  'auth_type' => 'Auth-Type',
+  'usr_modem_training_time' => 'USR-Modem-Training-Time',
+  'ascend_cbcp_enable' => 'Ascend-CBCP-Enable',
+  'x_ascend_ipx_route' => 'X-Ascend-IPX-Route',
+  'ascend_redirect_number' => 'Ascend-Redirect-Number',
+  'h323_credit_time' => 'h323-credit-time',
+  'ascend_appletalk_route' => 'Ascend-Appletalk-Route',
+  'shiva_link_protocol' => 'Shiva-Link-Protocol',
+  'x_ascend_fr_circuit_name' => 'X-Ascend-FR-Circuit-Name',
+  'client_id' => 'Client-Id',
+  'usr_appletalk' => 'USR-Appletalk',
+  'usr_mpip_tunnel_originat' => 'USR-MPIP-Tunnel-Originator',
+  'annex_output_filter' => 'Annex-Output-Filter',
+  'pvc_circuit_padding' => 'PVC_Circuit_Padding',
+  'x_ascend_minimum_channel' => 'X-Ascend-Minimum-Channels',
+  'h323_time_and_day' => 'h323-time-and-day',
+  'ascend_ipx_header_compre' => 'Ascend-IPX-Header-Compression',
+  'termination_action' => 'Termination-Action',
+  'x_ascend_modem_portno' => 'X-Ascend-Modem-PortNo',
+  'acct_tunnel_packets_lost' => 'Acct-Tunnel-Packets-Lost',
+  'framed_filter_id' => 'Framed-Filter-Id',
+  'usr_ccp_algorithm' => 'USR-CCP-Algorithm',
+  'ascend_token_expiry' => 'Ascend-Token-Expiry',
+  'annex_secondary_nbns_ser' => 'Annex-Secondary-NBNS-Server',
+  'usr_et_bridge_call_outpu' => 'USR-ET-Bridge-Call-Output-Filte',
+  'acc_modem_error_protocol' => 'Acc-Modem-Error-Protocol',
+  'acc_request_type' => 'Acc-Request-Type',
+  'x_ascend_ipx_peer_mode' => 'X-Ascend-IPX-Peer-Mode',
+  'ascend_ppp_vj_slot_comp' => 'Ascend-PPP-VJ-Slot-Comp',
+  'cisco_presession_time' => 'Cisco-PreSession-Time',
+  'usr_chat_script_name' => 'USR-Chat-Script-Name',
+  'ascend_fr_circuit_name' => 'Ascend-FR-Circuit-Name',
+  'ascend_expect_callback' => 'Ascend-Expect-Callback',
+  'framed_mtu' => 'Framed-MTU',
+  'ascend_port_redir_protoc' => 'Ascend-Port-Redir-Protocol',
+  'usr_pw_vpn_name' => 'USR-PW_VPN_Name',
+  'ascend_nas_port_format' => 'Ascend-NAS-Port-Format',
+  'shasta_vpn_name' => 'Shasta-VPN-Name',
+  'usr_dtr_true_timeout' => 'USR-DTR-True-Timeout',
+  'ascend_third_prompt' => 'Ascend-Third-Prompt',
+  'connect_rate' => 'Connect-Rate',
+  'usr_block_error_count_li' => 'USR-Block-Error-Count-Limit',
+  'called_station_id' => 'Called-Station-Id',
+  'usr_pw_cutoff' => 'USR-PW_Cutoff',
+  'ascend_data_rate' => 'Ascend-Data-Rate',
+  'x_ascend_ts_idle_mode' => 'X-Ascend-TS-Idle-Mode',
+  'ascend_x25_pad_prompt' => 'Ascend-X25-Pad-Prompt',
+  'x_ascend_dhcp_reply' => 'X-Ascend-DHCP-Reply',
+  'acc_nbns_server_pri' => 'Acc-Nbns-Server-Pri',
+  'ascend_call_filter' => 'Ascend-Call-Filter',
+  'acc_tunnel_secret' => 'Acc-Tunnel-Secret',
+  'usr_simplified_v42bis_us' => 'USR-Simplified-V42bis-Usage',
+  'bind_int_context' => 'Bind_Int_Context',
+  'erx_virtual_router_name' => 'ERX-Virtual-Router-Name',
+  'crypt_password' => 'Crypt-Password',
+  'challenge_state' => 'Challenge-State',
+  'ascend_client_secondary_' => 'Ascend-Client-Secondary-DNS',
+  'strip_user_name' => 'Strip-User-Name',
+  'x_ascend_user_acct_host' => 'X-Ascend-User-Acct-Host',
+  'x_ascend_route_ip' => 'X-Ascend-Route-IP',
+  'x_ascend_assign_ip_clien' => 'X-Ascend-Assign-IP-Client',
+  'usr_mbi_ct_bchannel_used' => 'USR-Mbi_Ct_BChannel_Used',
+  'ascend_x25_profile_name' => 'Ascend-X25-Profile-Name',
+  'usr_call_type' => 'USR-Call-Type',
+  'x_ascend_user_acct_base' => 'X-Ascend-User-Acct-Base',
+  'acct_output_gigawords' => 'Acct-Output-Gigawords',
+  'usr_rmmie_firmware_build' => 'USR-RMMIE-Firmware-Build-Date',
+  'ascend_fr_link_status_dl' => 'Ascend-FR-Link-Status-DLCI',
+  'login_lat_port' => 'Login-LAT-Port',
+  'usr_call_arrival_in_gmt' => 'USR-Call-Arrival-in-GMT',
+  'acct_mcast_in_octets' => 'Acct_Mcast_In_Octets',
+  'erx_sa_validate' => 'ERX-Sa-Validate',
+  'ascend_service_type' => 'Ascend-Service-Type',
+  'ascend_x25_nui_password_' => 'Ascend-X25-Nui-Password-Prompt',
+  'usr_pw_vpn_gateway' => 'USR-PW_VPN_Gateway',
+  'ascend_fr_dce_n392' => 'Ascend-FR-DCE-N392',
+  'acc_ip_compression' => 'Acc-Ip-Compression',
+  'lac_real_port_type' => 'LAC_Real_Port_Type',
+  'ascend_if_netmask' => 'Ascend-IF-Netmask',
+  'acct_session_start_time' => 'Acct-Session-Start-Time',
+  'ms_chap_nt_enc_pw' => 'MS-CHAP-NT-Enc-PW',
+  'ascend_port_redir_portnu' => 'Ascend-Port-Redir-Portnum',
+  'mcast_maxgroups' => 'Mcast_MaxGroups',
+  'x_ascend_home_agent_ip_a' => 'X-Ascend-Home-Agent-IP-Addr',
+  'ascend_cache_time' => 'Ascend-Cache-Time',
+  'x_ascend_data_svc' => 'X-Ascend-Data-Svc',
+  'erx_tunnel_virtual_route' => 'ERX-Tunnel-Virtual-Router',
+  'usr_re_chap_timeout' => 'USR-Re-Chap-Timeout',
+  'x_ascend_ppp_vj_1172' => 'X-Ascend-PPP-VJ-1172',
+  'usr_igmp_routing' => 'USR-IGMP-Routing',
+  'h323_prompt_id' => 'h323-prompt-id',
+  'le_terminate_detail' => 'LE-Terminate-Detail',
+  'acc_ml_clear_threshold' => 'Acc-ML-Clear-Threshold',
+  'x_ascend_ip_direct' => 'X-Ascend-IP-Direct',
+  'nas_port' => 'NAS-Port',
+  'x_ascend_data_rate' => 'X-Ascend-Data-Rate',
+  'usr_ip_call_input_filter' => 'USR-IP-Call-Input-Filter',
+  'ascend_auth_type' => 'Ascend-Auth-Type',
+  'x_ascend_preempt_limit' => 'X-Ascend-Preempt-Limit',
+  'h323_credit_amount' => 'h323-credit-amount',
+  'usr_reply_script1' => 'USR-Reply-Script1',
+  'usr_et_bridge_input_filt' => 'USR-ET-Bridge-Input-Filter',
+  'current_time' => 'Current-Time',
+  'cisco_xmit_rate' => 'Cisco-Xmit-Rate',
+  'ascend_authen_alias' => 'Ascend-Authen-Alias',
+  'x_ascend_session_svr_key' => 'X-Ascend-Session-Svr-Key',
+  'acc_dialout_auth_mode' => 'Acc-Dialout-Auth-Mode',
+  'usr_event_date_time' => 'USR-Event-Date-Time',
+  'x_ascend_ipx_node_addr' => 'X-Ascend-IPX-Node-Addr',
+  'ascend_primary_home_agen' => 'Ascend-Primary-Home-Agent',
+  'x_ascend_user_acct_time' => 'X-Ascend-User-Acct-Time',
+  'usr_at_call_output_filte' => 'USR-AT-Call-Output-Filter',
+  'acc_output_errors' => 'Acc-Output-Errors',
+  'usr_ipx_rip_output_filte' => 'USR-IPX-RIP-Output-Filter',
+  'x_ascend_pri_number_type' => 'X-Ascend-PRI-Number-Type',
+  'bind_l2tp_tunnel_name' => 'Bind_L2TP_Tunnel_Name',
+  'replicate_to_realm' => 'Replicate-To-Realm',
+  'usr_at_zip_input_filter' => 'USR-AT-Zip-Input-Filter',
+  'annex_mrru' => 'Annex-MRRU',
+  'event_timestamp' => 'Event-Timestamp',
+  'ascend_pre_input_packets' => 'Ascend-Pre-Input-Packets',
+  'h323_call_origin' => 'h323-call-origin',
+  'x_ascend_fr_type' => 'X-Ascend-FR-Type',
+  'x_ascend_token_idle' => 'X-Ascend-Token-Idle',
+  'usr_igmp_query_interval' => 'USR-IGMP-Query-Interval',
+  'ascend_atm_vci' => 'Ascend-ATM-Vci',
+  'usr_port_tap_output' => 'USR-Port-Tap-Output',
+  'session' => 'Session',
+  'ascend_uu_info' => 'Ascend-UU-Info',
+  'ms_mppe_recv_key' => 'MS-MPPE-Recv-Key',
+  'usr_secondary_dns_server' => 'USR-Secondary_DNS_Server',
+  'x_ascend_tunneling_proto' => 'X-Ascend-Tunneling-Protocol',
+  'acc_dial_port_index' => 'Acc-Dial-Port-Index',
+  'cisco_nas_port' => 'Cisco-NAS-Port',
+  'usr_send_script1' => 'USR-Send-Script1',
+  'usr_tunnel_security' => 'USR-Tunnel-Security',
+  'arap_security' => 'ARAP-Security',
+  'tunnel_preference' => 'Tunnel-Preference',
+  'usr_reply_script4' => 'USR-Reply-Script4',
+  'h323_currency_type' => 'h323-currency-type',
+  'usr_rmmie_status' => 'USR-RMMIE-Status',
+  'ascend_shared_profile_en' => 'Ascend-Shared-Profile-Enable',
+  'annex_syslog_tap' => 'Annex-Syslog-Tap',
+  'usr_send_script4' => 'USR-Send-Script4',
+  'acc_clearing_location' => 'Acc-Clearing-Location',
+  'annex_disconnect_reason' => 'Annex-Disconnect-Reason',
+  'x_ascend_dhcp_maximum_le' => 'X-Ascend-DHCP-Maximum-Leases',
+  'usr_at_input_filter' => 'USR-AT-Input-Filter',
+  'usr_auth_mode' => 'USR-Auth-Mode',
+  'shiva_session_id' => 'Shiva-Session-Id',
+  'usr_expected_voltage' => 'USR-Expected-Voltage',
+  'ascend_owner_ip_addr' => 'Ascend-Owner-IP-Addr',
+  'ascend_atm_direct_profil' => 'Ascend-ATM-Direct-Profile',
+  'usr_pw_usr_ofilter_ipx' => 'USR-PW_USR_OFilter_IPX',
+  'framed_routing' => 'Framed-Routing',
+  'pam_auth' => 'Pam-Auth',
+  'usr_interface_index' => 'USR-Interface-Index',
+  'x_ascend_transit_number' => 'X-Ascend-Transit-Number',
+  'usr_end_time' => 'USR-End-Time',
+  'x_ascend_assign_ip_pool' => 'X-Ascend-Assign-IP-Pool',
+  'ms_secondary_nbns_server' => 'MS-Secondary-NBNS-Server',
+  'bind_dot1q_vlan_tag_id' => 'Bind_Dot1q_Vlan_Tag_Id',
+  'acct_tunnel_connection' => 'Acct-Tunnel-Connection',
+  'tunnel_retransmit' => 'Tunnel_Retransmit',
+  'x_ascend_backup' => 'X-Ascend-Backup',
+  'usr_bearer_capabilities' => 'USR-Bearer-Capabilities',
+  'ascend_calling_id_type_o' => 'Ascend-Calling-Id-Type-Of-Num',
+  'shiva_acct_serv_switch' => 'Shiva-Acct-Serv-Switch',
+  'ascend_h323_conference_i' => 'Ascend-H323-Conference-Id',
+  'acct_authentic' => 'Acct-Authentic',
+  'x_ascend_force_56' => 'X-Ascend-Force-56',
+  'framed_appletalk_network' => 'Framed-AppleTalk-Network',
+  'reply_message' => 'Reply-Message',
+  'annex_addr_resolution_pr' => 'Annex-Addr-Resolution-Protocol',
+  'class' => 'Class',
+  'h323_conf_id' => 'h323-conf-id',
+  'ascend_cbcp_delay' => 'Ascend-CBCP-Delay',
+  'ascend_dropped_octets' => 'Ascend-Dropped-Octets',
+  'ascend_h323_dialed_time' => 'Ascend-H323-Dialed-Time',
+  'usr_local_ip_address' => 'USR-Local-IP-Address',
+  'ascend_x25_x121_address' => 'Ascend-X25-X121-Address',
+  'ascend_destination_nas_p' => 'Ascend-Destination-Nas-Port',
+  'annex_local_ip_address' => 'Annex-Local-IP-Address',
+  'usr_at_output_filter' => 'USR-AT-Output-Filter',
+  'cisco_ip_pool_definition' => 'Cisco-IP-Pool-Definition',
+  'annex_domain_name' => 'Annex-Domain-Name',
+  'ascend_preempt_limit' => 'Ascend-Preempt-Limit',
+  'ascend_event_type' => 'Ascend-Event-Type',
+  'x_ascend_pre_input_octet' => 'X-Ascend-Pre-Input-Octets',
+  'exec_program_wait' => 'Exec-Program-Wait',
+
+  #NOMENT
+  'nomadix_ip_upsell' => 'Nomadix-IP-Upsell',
+
+  #NETC.NET.AU (RADIATOR?)
+  'authentication_type' => 'Authentication-Type',
+
+);
+
+1;
diff --git a/FS/FS/radius_usergroup.pm b/FS/FS/radius_usergroup.pm
new file mode 100644 (file)
index 0000000..647621d
--- /dev/null
@@ -0,0 +1,130 @@
+package FS::radius_usergroup;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+use FS::svc_acct;
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::radius_usergroup - Object methods for radius_usergroup records
+
+=head1 SYNOPSIS
+
+  use FS::radius_usergroup;
+
+  $record = new FS::radius_usergroup \%hash;
+  $record = new FS::radius_usergroup { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::radius_usergroup object links an account (see L<FS::svc_acct>) with a
+RADIUS group.  FS::radius_usergroup inherits from FS::Record.  The following
+fields are currently supported:
+
+=over 4
+
+=item usergroupnum - primary key
+
+=item svcnum - Account (see L<FS::svc_acct>).
+
+=item groupname - group name
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'radius_usergroup'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+#inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+#inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+#inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  $self->ut_numbern('usergroupnum')
+    || $self->ut_number('svcnum')
+    || $self->ut_foreign_key('svcnum','svc_acct','svcnum')
+    || $self->ut_text('groupname')
+  ;
+}
+
+=item svc_acct
+
+Returns the account associated with this record (see L<FS::svc_acct>).
+
+=cut
+
+sub svc_acct {
+  my $self = shift;
+  qsearchs('svc_acct', { svcnum => $self->svcnum } );
+}
+
+=back
+
+=head1 BUGS
+
+Don't let 'em get you down.
+
+=head1 SEE ALSO
+
+L<svc_acct>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/router.pm b/FS/FS/router.pm
new file mode 100755 (executable)
index 0000000..3f9459a
--- /dev/null
@@ -0,0 +1,156 @@
+package FS::router;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs qsearch );
+use FS::addr_block;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::router - Object methods for router records
+
+=head1 SYNOPSIS
+
+  use FS::router;
+
+  $record = new FS::router \%hash;
+  $record = new FS::router { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::router record describes a broadband router, such as a DSLAM or a wireless
+ access point.  FS::router inherits from FS::Record.  The following 
+fields are currently supported:
+
+=over 4
+
+=item routernum - primary key
+
+=item routername - descriptive name for the router
+
+=item svcnum - svcnum of the owning FS::svc_broadband, if appropriate
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'router'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error =
+    $self->ut_numbern('routernum')
+    || $self->ut_text('routername');
+  return $error if $error;
+
+  '';
+}
+
+=item addr_block
+
+Returns a list of FS::addr_block objects (address blocks) associated
+with this object.
+
+=cut
+
+sub addr_block {
+  my $self = shift;
+  return qsearch('addr_block', { routernum => $self->routernum });
+}
+
+=item router_field
+
+Returns a list of FS::router_field objects assigned to this object.
+
+=cut
+
+sub router_field {
+  my $self = shift;
+
+  return qsearch('router_field', { routernum => $self->routernum });
+}
+
+=item part_svc_router
+
+Returns a list of FS::part_svc_router objects associated with this 
+object.  This is unlikely to be useful for any purpose other than retrieving 
+the associated FS::part_svc objects.  See below.
+
+=cut
+
+sub part_svc_router {
+  my $self = shift;
+  return qsearch('part_svc_router', { routernum => $self->routernum });
+}
+
+=item part_svc
+
+Returns a list of FS::part_svc objects associated with this object.
+
+=cut
+
+sub part_svc {
+  my $self = shift;
+  return map { qsearchs('part_svc', { svcpart => $_->svcpart }) }
+      $self->part_svc_router;
+}
+
+=back
+
+=head1 VERSION
+
+$Id:
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::router, FS::addr_block, FS::router_field, FS::part_svc,
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/router_field.pm b/FS/FS/router_field.pm
new file mode 100755 (executable)
index 0000000..eee21ab
--- /dev/null
@@ -0,0 +1,146 @@
+package FS::router_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::part_router_field;
+use FS::router;
+
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::router_field - Object methods for router_field records
+
+=head1 SYNOPSIS
+
+  use FS::router_field;
+
+  $record = new FS::router_field \%hash;
+  $record = new FS::router_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+FS::router_field contains values of router xfields.  See FS::part_sb_field 
+for details on the xfield mechanism.
+
+=over 4
+
+=item routerfieldpart - Type of router_field as defined by 
+FS::part_router_field
+
+=item routernum - The FS::router to which this value belongs.
+
+=item value - The contents of the field.
+
+=back
+
+=head1 METHODS
+
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'router_field'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  return "routernum must be defined" unless $self->routernum;
+  return "routerfieldpart must be defined" unless $self->routerfieldpart;
+
+  my $part_router_field = $self->part_router_field;
+  $_ = $self->value;
+
+  my $check_block = $part_router_field->check_block;
+  if ($check_block) {
+    $@ = '';
+    my $error = (eval($check_block) or $@);
+    return $error if $error;
+    $self->setfield('value' => $_);
+  }
+
+  ''; #no error
+}
+
+=item part_router_field
+
+Returns a reference to the FS:part_router_field that defines this 
+FS::router_field
+
+=cut
+
+sub part_router_field {
+  my $self = shift;
+
+  return qsearchs('part_router_field', 
+    { routerfieldpart => $self->routerfieldpart });
+}
+
+=item router
+
+Returns a reference to the FS::router to which this FS::router_field 
+belongs.
+
+=cut
+
+sub router {
+  my $self = shift;
+
+  return qsearchs('router', { routernum => $self->routernum });
+}
+
+=back
+
+=head1 VERSION
+
+$Id: 
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::router, FS::router_block, FS::router_field,  
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/sb_field.pm b/FS/FS/sb_field.pm
new file mode 100755 (executable)
index 0000000..d4eb378
--- /dev/null
@@ -0,0 +1,148 @@
+package FS::sb_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::part_sb_field;
+
+use UNIVERSAL qw( can );
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::sb_field - Object methods for sb_field records
+
+=head1 SYNOPSIS
+
+  use FS::sb_field;
+
+  $record = new FS::sb_field \%hash;
+  $record = new FS::sb_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+See L<FS::part_sb_field> for details on this table's mission in life.
+FS::sb_field contains the actual values of the xfields defined in
+part_sb_field.
+
+The following fields are supported:
+
+=over 4
+
+=item sbfieldpart - Type of sb_field as defined by FS::part_sb_field
+
+=item svcnum - The svc_broadband to which this value belongs.
+
+=item value - The contents of the field.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'sb_field'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks the value against the check_block of the corresponding part_sb_field.
+Returns whatever the check_block returned (unless the check_block dies, in 
+which case check returns the die message).  Therefore, if the check_block 
+wants to allow the value to be stored, it must return false.  See 
+L<FS::part_sb_field> for details.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  return "svcnum must be defined" unless $self->svcnum;
+  return "sbfieldpart must be defined" unless $self->sbfieldpart;
+
+  my $part_sb_field = $self->part_sb_field;
+
+  $_ = $self->value;
+
+  my $check_block = $self->part_sb_field->check_block;
+  if ($check_block) {
+    $@ = '';
+    my $error = (eval($check_block) or $@); # treat fatal errors as errors
+    return $error if $error;
+    $self->setfield('value' => $_);
+  }
+
+  ''; #no error
+}
+
+=item part_sb_field
+
+Returns a reference to the FS::part_sb_field that defines this FS::sb_field.
+
+=cut
+
+sub part_sb_field {
+  my $self = shift;
+
+  return qsearchs('part_sb_field', { sbfieldpart => $self->sbfieldpart });
+}
+
+=back
+
+=item svc_broadband
+
+Returns a reference to the FS::svc_broadband to which this value is attached.
+Nobody's ever going to use this function, but here it is anyway.
+
+=cut
+
+sub svc_broadband {
+  my $self = shift;
+
+  return qsearchs('svc_broadband', { svcnum => $self->svcnum });
+}
+
+=head1 VERSION
+
+$Id: 
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_broadband>, schema.html
+from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/session.pm b/FS/FS/session.pm
new file mode 100644 (file)
index 0000000..de0f2a7
--- /dev/null
@@ -0,0 +1,269 @@
+package FS::session;
+
+use strict;
+use vars qw( @ISA $conf $start $stop );
+use FS::UID qw( dbh );
+use FS::Record qw( qsearchs );
+use FS::svc_acct;
+use FS::port;
+use FS::nas;
+
+@ISA = qw(FS::Record);
+
+$FS::UID::callback{'FS::session'} = sub {
+  $conf = new FS::Conf;
+  $start = $conf->exists('session-start') ? $conf->config('session-start') : '';
+  $stop = $conf->exists('session-stop') ? $conf->config('session-stop') : '';
+};
+
+=head1 NAME
+
+FS::session - Object methods for session records
+
+=head1 SYNOPSIS
+
+  use FS::session;
+
+  $record = new FS::session \%hash;
+  $record = new FS::session {
+    'portnum' => 1,
+    'svcnum'  => 2,
+    'login'   => $timestamp,
+    'logout'  => $timestamp,
+  };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->nas_heartbeat($timestamp);
+
+=head1 DESCRIPTION
+
+An FS::session object represents an user login session.  FS::session inherits
+from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item sessionnum - primary key
+
+=item portnum - NAS port for this session - see L<FS::port>
+
+=item svcnum - User for this session - see L<FS::svc_acct>
+
+=item login - timestamp indicating the beginning of this user session.
+
+=item logout - timestamp indicating the end of this user session.  May be null,
+               which indicates a currently open session.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new session.  To add the session to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'session'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.  If the `login' field is empty, it is replaced with
+the current time.
+
+=cut
+
+sub insert {
+  my $self = shift;
+  my $error;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  $error = $self->check;
+  return $error if $error;
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  if ( qsearchs('session', { 'portnum' => $self->portnum, 'logout' => '' } ) ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "a session on that port is already open!";
+  }
+
+  $self->setfield('login', time()) unless $self->getfield('login');
+
+  $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $self->nas_heartbeat($self->getfield('login'));
+
+  #session-starting callback
+    #redundant with heartbeat, yuck
+  my $port = qsearchs('port',{'portnum'=>$self->portnum});
+  my $nas = qsearchs('nas',{'nasnum'=>$port->nasnum});
+    #kcuy
+  my( $ip, $nasip, $nasfqdn ) = ( $port->ip, $nas->nasip, $nas->nasfqdn );
+  system( eval qq("$start") ) if $start;
+  
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.  If the `logout' field is empty,
+it is replaced with the current time.
+
+=cut
+
+sub replace {
+  my($self, $old) = @_;
+  my $error;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $error = $self->check;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $self->setfield('logout', time()) unless $self->getfield('logout');
+
+  $error = $self->SUPER::replace($old);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $self->nas_heartbeat($self->getfield('logout'));
+
+  #session-ending callback
+  #redundant with heartbeat, yuck
+  my $port = qsearchs('port',{'portnum'=>$self->portnum});
+  my $nas = qsearchs('nas',{'nasnum'=>$port->nasnum});
+    #kcuy
+  my( $ip, $nasip, $nasfqdn ) = ( $port->ip, $nas->nasip, $nas->nasfqdn );
+  system( eval qq("$stop") ) if $stop;
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item check
+
+Checks all fields to make sure this is a valid session.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+  my $error =
+    $self->ut_numbern('sessionnum')
+    || $self->ut_number('portnum')
+    || $self->ut_number('svcnum')
+    || $self->ut_numbern('login')
+    || $self->ut_numbern('logout')
+  ;
+  return $error if $error;
+  return "Unknown svcnum"
+    unless qsearchs('svc_acct', { 'svcnum' => $self->svcnum } );
+  '';
+}
+
+=item nas_heartbeat
+
+Heartbeats the nas associated with this session (see L<FS::nas>).
+
+=cut
+
+sub nas_heartbeat {
+  my $self = shift;
+  my $port = qsearchs('port',{'portnum'=>$self->portnum});
+  my $nas = qsearchs('nas',{'nasnum'=>$port->nasnum});
+  $nas->heartbeat(shift);
+}
+
+=item svc_acct
+
+Returns the svc_acct record associated with this session (see L<FS::svc_acct>).
+
+=cut
+
+sub svc_acct {
+  my $self = shift;
+  qsearchs('svc_acct', { 'svcnum' => $self->svcnum } );
+}
+
+=back
+
+=head1 VERSION
+
+$Id: session.pm,v 1.7 2001-04-15 13:35:12 ivan Exp $
+
+=head1 BUGS
+
+Maybe you shouldn't be able to insert a session if there's currently an open
+session on that port.  Or maybe the open session on that port should be flagged
+as problematic?  autoclosed?  *sigh*
+
+Hmm, sessions refer to current svc_acct records... probably need to constrain
+deletions to svc_acct records such that no svc_acct records are deleted which
+have a session (even if long-closed).
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm
new file mode 100644 (file)
index 0000000..87b6097
--- /dev/null
@@ -0,0 +1,381 @@
+package FS::svc_Common;
+
+use strict;
+use vars qw( @ISA $noexport_hack );
+use FS::Record qw( qsearchs fields dbh );
+use FS::cust_svc;
+use FS::part_svc;
+use FS::queue;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::svc_Common - Object method for all svc_ records
+
+=head1 SYNOPSIS
+
+use FS::svc_Common;
+
+@ISA = qw( FS::svc_Common );
+
+=head1 DESCRIPTION
+
+FS::svc_Common is intended as a base class for table-specific classes to
+inherit from, i.e. FS::svc_acct.  FS::svc_Common inherits from FS::Record.
+
+=head1 METHODS
+
+=over 4
+
+=item insert [ JOBNUM_ARRAYREF ]
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
+defined.  An FS::cust_svc record will be created and inserted.
+
+If an arrayref is passed as parameter, the B<jobnum>s of any export jobs will
+be added to the array.
+
+=cut
+
+sub insert {
+  my $self = shift;
+  local $FS::queue::jobnums = shift if @_;
+  my $error;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $error = $self->check;
+  return $error if $error;
+
+  my $svcnum = $self->svcnum;
+  my $cust_svc;
+  unless ( $svcnum ) {
+    $cust_svc = new FS::cust_svc ( {
+      #hua?# 'svcnum'  => $svcnum,
+      'pkgnum'  => $self->pkgnum,
+      'svcpart' => $self->svcpart,
+    } );
+    $error = $cust_svc->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+    $svcnum = $self->svcnum($cust_svc->svcnum);
+  } else {
+    $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
+    unless ( $cust_svc ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "no cust_svc record found for svcnum ". $self->svcnum;
+    }
+    $self->pkgnum($cust_svc->pkgnum);
+    $self->svcpart($cust_svc->svcpart);
+  }
+
+  $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  #new-style exports!
+  unless ( $noexport_hack ) {
+    foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+      my $error = $part_export->export_insert($self);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "exporting to ". $part_export->exporttype.
+               " (transaction rolled back): $error";
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item delete
+
+Deletes this account from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+The corresponding FS::cust_svc record will be deleted as well.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  my $error;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $svcnum = $self->svcnum;
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $error = $self->SUPER::delete;
+  return $error if $error;
+
+  #new-style exports!
+  unless ( $noexport_hack ) {
+    foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+      my $error = $part_export->export_delete($self);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "exporting to ". $part_export->exporttype.
+               " (transaction rolled back): $error";
+      }
+    }
+  }
+
+  return $error if $error;
+
+  my $cust_svc = $self->cust_svc;
+  $error = $cust_svc->delete;
+  return $error if $error;
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub replace {
+  my ($new, $old) = (shift, shift);
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $new->SUPER::replace($old);
+  if ($error) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  #new-style exports!
+  unless ( $noexport_hack ) {
+    foreach my $part_export ( $new->cust_svc->part_svc->part_export ) {
+      my $error = $part_export->export_replace($new,$old);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error exporting to ". $part_export->exporttype.
+               " (transaction rolled back): $error";
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+}
+
+
+=item setfixed
+
+Sets any fixed fields for this service (see L<FS::part_svc>).  If there is an
+error, returns the error, otherwise returns the FS::part_svc object (use ref()
+to test the return).  Usually called by the check method.
+
+=cut
+
+sub setfixed {
+  my $self = shift;
+  $self->setx('F');
+}
+
+=item setdefault
+
+Sets all fields to their defaults (see L<FS::part_svc>), overriding their
+current values.  If there is an error, returns the error, otherwise returns
+the FS::part_svc object (use ref() to test the return).
+
+=cut
+
+sub setdefault {
+  my $self = shift;
+  $self->setx('D');
+}
+
+sub setx {
+  my $self = shift;
+  my $x = shift;
+
+  my $error;
+
+  $error =
+    $self->ut_numbern('svcnum')
+  ;
+  return $error if $error;
+
+  #get part_svc
+  my $svcpart;
+  if ( $self->svcnum ) {
+    my $cust_svc = $self->cust_svc;
+    return "Unknown svcnum" unless $cust_svc; 
+    $svcpart = $cust_svc->svcpart;
+  } else {
+    $svcpart = $self->getfield('svcpart');
+  }
+  my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
+  return "Unkonwn svcpart" unless $part_svc;
+
+  #set default/fixed/whatever fields from part_svc
+  my $table = $self->table;
+  foreach my $field ( grep { $_ ne 'svcnum' } fields($table) ) {
+    my $part_svc_column = $part_svc->part_svc_column($field);
+    if ( $part_svc_column->columnflag eq $x ) {
+      $self->setfield( $field, $part_svc_column->columnvalue );
+    }
+  }
+
+ $part_svc;
+
+}
+
+=item cust_svc
+
+Returns the cust_svc record associated with this svc_ record, as a FS::cust_svc
+object (see L<FS::cust_svc>).
+
+=cut
+
+sub cust_svc {
+  my $self = shift;
+  qsearchs('cust_svc', { 'svcnum' => $self->svcnum } );
+}
+
+=item suspend
+
+Runs export_suspend callbacks.
+
+=cut
+
+sub suspend {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  #new-style exports!
+  unless ( $noexport_hack ) {
+    foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+      my $error = $part_export->export_suspend($self);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error exporting to ". $part_export->exporttype.
+               " (transaction rolled back): $error";
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item unsuspend
+
+Runs export_unsuspend callbacks.
+
+=cut
+
+sub unsuspend {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  #new-style exports!
+  unless ( $noexport_hack ) {
+    foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
+      my $error = $part_export->export_unsuspend($self);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error exporting to ". $part_export->exporttype.
+               " (transaction rolled back): $error";
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
+=item cancel
+
+Stub - returns false (no error) so derived classes don't need to define these
+methods.  Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=cut
+
+sub cancel { ''; }
+
+=back
+
+=head1 VERSION
+
+$Id: svc_Common.pm,v 1.12 2002-06-14 11:22:53 ivan Exp $
+
+=head1 BUGS
+
+The setfixed method return value.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
+from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm
new file mode 100644 (file)
index 0000000..5b8107f
--- /dev/null
@@ -0,0 +1,1212 @@
+package FS::svc_acct;
+
+use strict;
+use vars qw( @ISA $DEBUG $me $conf
+             $dir_prefix @shells $usernamemin
+             $usernamemax $passwordmin $passwordmax
+             $username_ampersand $username_letter $username_letterfirst
+             $username_noperiod $username_nounderscore $username_nodash
+             $username_uppercase
+             $welcome_template $welcome_from $welcome_subject $welcome_mimetype
+             $smtpmachine
+             $radius_password $radius_ip
+             $dirhash
+             @saltset @pw_set );
+use Carp;
+use Fcntl qw(:flock);
+use FS::UID qw( datasrc );
+use FS::Conf;
+use FS::Record qw( qsearch qsearchs fields dbh );
+use FS::svc_Common;
+use FS::cust_svc;
+use FS::part_svc;
+use FS::svc_acct_pop;
+use FS::cust_main_invoice;
+use FS::svc_domain;
+use FS::raddb;
+use FS::queue;
+use FS::radius_usergroup;
+use FS::export_svc;
+use FS::part_export;
+use FS::Msgcat qw(gettext);
+
+@ISA = qw( FS::svc_Common );
+
+$DEBUG = 0;
+$me = '[FS::svc_acct]';
+
+#ask FS::UID to run this stuff for us later
+$FS::UID::callback{'FS::svc_acct'} = sub { 
+  $conf = new FS::Conf;
+  $dir_prefix = $conf->config('home');
+  @shells = $conf->config('shells');
+  $usernamemin = $conf->config('usernamemin') || 2;
+  $usernamemax = $conf->config('usernamemax');
+  $passwordmin = $conf->config('passwordmin') || 6;
+  $passwordmax = $conf->config('passwordmax') || 8;
+  $username_letter = $conf->exists('username-letter');
+  $username_letterfirst = $conf->exists('username-letterfirst');
+  $username_noperiod = $conf->exists('username-noperiod');
+  $username_nounderscore = $conf->exists('username-nounderscore');
+  $username_nodash = $conf->exists('username-nodash');
+  $username_uppercase = $conf->exists('username-uppercase');
+  $username_ampersand = $conf->exists('username-ampersand');
+  $dirhash = $conf->config('dirhash') || 0;
+  if ( $conf->exists('welcome_email') ) {
+    $welcome_template = new Text::Template (
+      TYPE   => 'ARRAY',
+      SOURCE => [ map "$_\n", $conf->config('welcome_email') ]
+    ) or warn "can't create welcome email template: $Text::Template::ERROR";
+    $welcome_from = $conf->config('welcome_email-from'); # || 'your-isp-is-dum'
+    $welcome_subject = $conf->config('welcome_email-subject') || 'Welcome';
+    $welcome_mimetype = $conf->config('welcome_email-mimetype') || 'text/plain';
+  } else {
+    $welcome_template = '';
+    $welcome_from = '';
+    $welcome_subject = '';
+    $welcome_mimetype = '';
+  }
+  $smtpmachine = $conf->config('smtpmachine');
+  $radius_password = $conf->config('radius-password') || 'Password';
+  $radius_ip = $conf->config('radius-ip') || 'Framed-IP-Address';
+};
+
+@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+@pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
+
+sub _cache {
+  my $self = shift;
+  my ( $hashref, $cache ) = @_;
+  if ( $hashref->{'svc_acct_svcnum'} ) {
+    $self->{'_domsvc'} = FS::svc_domain->new( {
+      'svcnum'   => $hashref->{'domsvc'},
+      'domain'   => $hashref->{'svc_acct_domain'},
+      'catchall' => $hashref->{'svc_acct_catchall'},
+    } );
+  }
+}
+
+=head1 NAME
+
+FS::svc_acct - Object methods for svc_acct records
+
+=head1 SYNOPSIS
+
+  use FS::svc_acct;
+
+  $record = new FS::svc_acct \%hash;
+  $record = new FS::svc_acct { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->suspend;
+
+  $error = $record->unsuspend;
+
+  $error = $record->cancel;
+
+  %hash = $record->radius;
+
+  %hash = $record->radius_reply;
+
+  %hash = $record->radius_check;
+
+  $domain = $record->domain;
+
+  $svc_domain = $record->svc_domain;
+
+  $email = $record->email;
+
+  $seconds_since = $record->seconds_since($timestamp);
+
+=head1 DESCRIPTION
+
+An FS::svc_acct object represents an account.  FS::svc_acct inherits from
+FS::svc_Common.  The following fields are currently supported:
+
+=over 4
+
+=item svcnum - primary key (assigned automatcially for new accounts)
+
+=item username
+
+=item _password - generated if blank
+
+=item sec_phrase - security phrase
+
+=item popnum - Point of presence (see L<FS::svc_acct_pop>)
+
+=item uid
+
+=item gid
+
+=item finger - GECOS
+
+=item dir - set automatically if blank (and uid is not)
+
+=item shell
+
+=item quota - (unimplementd)
+
+=item slipip - IP address
+
+=item seconds - 
+
+=item domsvc - svcnum from svc_domain
+
+=item radius_I<Radius_Attribute> - I<Radius-Attribute>
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new account.  To add the account to the database, see L<"insert">.
+
+=cut
+
+sub table { 'svc_acct'; }
+
+=item insert
+
+Adds this account to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
+defined.  An FS::cust_svc record will be created and inserted.
+
+The additional field I<usergroup> can optionally be defined; if so it should
+contain an arrayref of group names.  See L<FS::radius_usergroup>.  (used in
+sqlradius export only)
+
+(TODOC: L<FS::queue> and L<freeside-queued>)
+
+(TODOC: new exports!)
+
+=cut
+
+sub insert {
+  my $self = shift;
+  my $error;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $error = $self->check;
+  return $error if $error;
+
+  #no, duplicate checking just got a whole lot more complicated
+  #(perhaps keep this check with a config option to turn on?)
+
+  #return gettext('username_in_use'). ": ". $self->username
+  #  if qsearchs( 'svc_acct', { 'username' => $self->username,
+  #                             'domsvc'   => $self->domsvc,
+  #                           } );
+
+  if ( $self->svcnum ) {
+    my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$self->svcnum});
+    unless ( $cust_svc ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "no cust_svc record found for svcnum ". $self->svcnum;
+    }
+    $self->pkgnum($cust_svc->pkgnum);
+    $self->svcpart($cust_svc->svcpart);
+  }
+
+  #new duplicate username checking
+
+  my $part_svc = qsearchs('part_svc', { 'svcpart' => $self->svcpart } );
+  unless ( $part_svc ) {
+    $dbh->rollback if $oldAutoCommit;
+    return 'unknown svcpart '. $self->svcpart;
+  }
+
+  my @dup_user = qsearch( 'svc_acct', { 'username' => $self->username } );
+  my @dup_userdomain = qsearch( 'svc_acct', { 'username' => $self->username,
+                                              'domsvc'   => $self->domsvc } );
+  my @dup_uid;
+  if ( $part_svc->part_svc_column('uid')->columnflag ne 'F'
+       && $self->username !~ /^(toor|(hyla)?fax)$/          ) {
+    @dup_uid = qsearch( 'svc_acct', { 'uid' => $self->uid } );
+  } else {
+    @dup_uid = ();
+  }
+
+  if ( @dup_user || @dup_userdomain || @dup_uid ) {
+    my $exports = FS::part_export::export_info('svc_acct');
+    my %conflict_user_svcpart;
+    my %conflict_userdomain_svcpart = ( $self->svcpart => 'SELF', );
+
+    foreach my $part_export ( $part_svc->part_export ) {
+
+      #this will catch to the same exact export
+      my @svcparts = map { $_->svcpart }
+        qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
+
+      #this will catch to exports w/same exporthost+type ???
+      #my @other_part_export = qsearch('part_export', {
+      #  'machine'    => $part_export->machine,
+      #  'exporttype' => $part_export->exporttype,
+      #} );
+      #foreach my $other_part_export ( @other_part_export ) {
+      #  push @svcparts, map { $_->svcpart }
+      #    qsearch('export_svc', { 'exportnum' => $part_export->exportnum });
+      #}
+
+      #my $nodomain = $exports->{$part_export->exporttype}{'nodomain'};
+      #silly kludge to avoid uninitialized value errors
+      my $nodomain = exists( $exports->{$part_export->exporttype}{'nodomain'} )
+                     ? $exports->{$part_export->exporttype}{'nodomain'}
+                     : '';
+      if ( $nodomain =~ /^Y/i ) {
+        $conflict_user_svcpart{$_} = $part_export->exportnum
+          foreach @svcparts;
+      } else {
+        $conflict_userdomain_svcpart{$_} = $part_export->exportnum
+          foreach @svcparts;
+      }
+    }
+
+    foreach my $dup_user ( @dup_user ) {
+      my $dup_svcpart = $dup_user->cust_svc->svcpart;
+      if ( exists($conflict_user_svcpart{$dup_svcpart}) ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "duplicate username: conflicts with svcnum ". $dup_user->svcnum.
+               " via exportnum ". $conflict_user_svcpart{$dup_svcpart};
+      }
+    }
+
+    foreach my $dup_userdomain ( @dup_userdomain ) {
+      my $dup_svcpart = $dup_userdomain->cust_svc->svcpart;
+      if ( exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "duplicate username\@domain: conflicts with svcnum ".
+               $dup_userdomain->svcnum. " via exportnum ".
+               $conflict_userdomain_svcpart{$dup_svcpart};
+      }
+    }
+
+    foreach my $dup_uid ( @dup_uid ) {
+      my $dup_svcpart = $dup_uid->cust_svc->svcpart;
+      if ( exists($conflict_user_svcpart{$dup_svcpart})
+           || exists($conflict_userdomain_svcpart{$dup_svcpart}) ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "duplicate uid: conflicts with svcnum". $dup_uid->svcnum.
+               "via exportnum ". $conflict_user_svcpart{$dup_svcpart}
+                                 || $conflict_userdomain_svcpart{$dup_svcpart};
+      }
+    }
+
+  }
+
+  #see?  i told you it was more complicated
+
+  my @jobnums;
+  $error = $self->SUPER::insert(\@jobnums);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $self->usergroup ) {
+    foreach my $groupname ( @{$self->usergroup} ) {
+      my $radius_usergroup = new FS::radius_usergroup ( {
+        svcnum    => $self->svcnum,
+        groupname => $groupname,
+      } );
+      my $error = $radius_usergroup->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+
+  #false laziness with sub replace (and cust_main)
+  my $queue = new FS::queue {
+    'svcnum' => $self->svcnum,
+    'job'    => 'FS::svc_acct::append_fuzzyfiles'
+  };
+  $error = $queue->insert($self->username);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "queueing job (transaction rolled back): $error";
+  }
+
+  my $cust_pkg = $self->cust_svc->cust_pkg;
+
+  if ( $cust_pkg ) {
+    my $cust_main = $cust_pkg->cust_main;
+
+    if ( $conf->exists('emailinvoiceauto') ) {
+      my @invoicing_list = $cust_main->invoicing_list;
+      push @invoicing_list, $self->email;
+      $cust_main->invoicing_list(\@invoicing_list);
+    }
+
+    #welcome email
+    my $to = '';
+    if ( $welcome_template && $cust_pkg ) {
+      my $to = join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list );
+      if ( $to ) {
+        my $wqueue = new FS::queue {
+          'svcnum' => $self->svcnum,
+          'job'    => 'FS::svc_acct::send_email'
+        };
+        my $error = $wqueue->insert(
+          'to'       => $to,
+          'from'     => $welcome_from,
+          'subject'  => $welcome_subject,
+          'mimetype' => $welcome_mimetype,
+          'body'     => $welcome_template->fill_in( HASH => {
+                          'custnum'  => $self->custnum,
+                          'username' => $self->username,
+                          'password' => $self->_password,
+                          'first'    => $cust_main->first,
+                          'last'     => $cust_main->getfield('last'),
+                          'pkg'      => $cust_pkg->part_pkg->pkg,
+                        } ),
+        );
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return "error queuing welcome email: $error";
+        }
+
+        foreach my $jobnum ( @jobnums ) {
+          my $error = $wqueue->depend_insert($jobnum);
+          if ( $error ) {
+            $dbh->rollback if $oldAutoCommit;
+            return "error queuing welcome email job dependancy: $error";
+          }
+        }
+
+      }
+
+    }
+
+  } # if ( $cust_pkg )
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+}
+
+=item delete
+
+Deletes this account from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+The corresponding FS::cust_svc record will be deleted as well.
+
+(TODOC: new exports!)
+
+=cut
+
+sub delete {
+  my $self = shift;
+
+  return "Can't delete an account which is a (svc_forward) source!"
+    if qsearch( 'svc_forward', { 'srcsvc' => $self->svcnum } );
+
+  return "Can't delete an account which is a (svc_forward) destination!"
+    if qsearch( 'svc_forward', { 'dstsvc' => $self->svcnum } );
+
+  return "Can't delete an account with (svc_www) web service!"
+    if qsearch( 'svc_www', { 'usersvc' => $self->usersvc } );
+
+  # what about records in session ? (they should refer to history table)
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $cust_main_invoice (
+    qsearch( 'cust_main_invoice', { 'dest' => $self->svcnum } )
+  ) {
+    unless ( defined($cust_main_invoice) ) {
+      warn "WARNING: something's wrong with qsearch";
+      next;
+    }
+    my %hash = $cust_main_invoice->hash;
+    $hash{'dest'} = $self->email;
+    my $new = new FS::cust_main_invoice \%hash;
+    my $error = $new->replace($cust_main_invoice);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  foreach my $svc_domain (
+    qsearch( 'svc_domain', { 'catchall' => $self->svcnum } )
+  ) {
+    my %hash = new FS::svc_domain->hash;
+    $hash{'catchall'} = '';
+    my $new = new FS::svc_domain \%hash;
+    my $error = $new->replace($svc_domain);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  foreach my $radius_usergroup (
+    qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } )
+  ) {
+    my $error = $radius_usergroup->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+}
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+The additional field I<usergroup> can optionally be defined; if so it should
+contain an arrayref of group names.  See L<FS::radius_usergroup>.  (used in
+sqlradius export only)
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+  my $error;
+  warn "$me replacing $old with $new\n" if $DEBUG;
+
+  return "Username in use"
+    if $old->username ne $new->username &&
+      qsearchs( 'svc_acct', { 'username' => $new->username,
+                               'domsvc'   => $new->domsvc,
+                             } );
+  {
+    #no warnings 'numeric';  #alas, a 5.006-ism
+    local($^W) = 0;
+    return "Can't change uid!" if $old->uid != $new->uid;
+  }
+
+  #change homdir when we change username
+  $new->setfield('dir', '') if $old->username ne $new->username;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  # redundant, but so $new->usergroup gets set
+  $error = $new->check;
+  return $error if $error;
+
+  $old->usergroup( [ $old->radius_groups ] );
+  warn "old groups: ". join(' ',@{$old->usergroup}). "\n" if $DEBUG;
+  warn "new groups: ". join(' ',@{$new->usergroup}). "\n" if $DEBUG;
+  if ( $new->usergroup ) {
+    #(sorta) false laziness with FS::part_export::sqlradius::_export_replace
+    my @newgroups = @{$new->usergroup};
+    foreach my $oldgroup ( @{$old->usergroup} ) {
+      if ( grep { $oldgroup eq $_ } @newgroups ) {
+        @newgroups = grep { $oldgroup ne $_ } @newgroups;
+        next;
+      }
+      my $radius_usergroup = qsearchs('radius_usergroup', {
+        svcnum    => $old->svcnum,
+        groupname => $oldgroup,
+      } );
+      my $error = $radius_usergroup->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error deleting radius_usergroup $oldgroup: $error";
+      }
+    }
+
+    foreach my $newgroup ( @newgroups ) {
+      my $radius_usergroup = new FS::radius_usergroup ( {
+        svcnum    => $new->svcnum,
+        groupname => $newgroup,
+      } );
+      my $error = $radius_usergroup->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error adding radius_usergroup $newgroup: $error";
+      }
+    }
+
+  }
+
+  $error = $new->SUPER::replace($old);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error if $error;
+  }
+
+  if ( $new->username ne $old->username ) {
+    #false laziness with sub insert (and cust_main)
+    my $queue = new FS::queue {
+      'svcnum' => $new->svcnum,
+      'job'    => 'FS::svc_acct::append_fuzzyfiles'
+    };
+    $error = $queue->insert($new->username);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "queueing job (transaction rolled back): $error";
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+}
+
+=item suspend
+
+Suspends this account by prefixing *SUSPENDED* to the password.  If there is an
+error, returns the error, otherwise returns false.
+
+Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+Calls any export-specific suspend hooks.
+
+=cut
+
+sub suspend {
+  my $self = shift;
+  my %hash = $self->hash;
+  unless ( $hash{_password} =~ /^\*SUSPENDED\* /
+           || $hash{_password} eq '*'
+         ) {
+    $hash{_password} = '*SUSPENDED* '.$hash{_password};
+    my $new = new FS::svc_acct ( \%hash );
+    my $error = $new->replace($self);
+    return $error if $error;
+  }
+
+  $self->SUPER::suspend;
+}
+
+=item unsuspend
+
+Unsuspends this account by removing *SUSPENDED* from the password.  If there is
+an error, returns the error, otherwise returns false.
+
+Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+Calls any export-specific unsuspend hooks.
+
+=cut
+
+sub unsuspend {
+  my $self = shift;
+  my %hash = $self->hash;
+  if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
+    $hash{_password} = $1;
+    my $new = new FS::svc_acct ( \%hash );
+    my $error = $new->replace($self);
+    return $error if $error;
+  }
+
+  $self->SUPER::unsuspend;
+}
+
+=item cancel
+
+Just returns false (no error) for now.
+
+Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item check
+
+Checks all fields to make sure this is a valid service.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+Sets any fixed values; see L<FS::part_svc>.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my($recref) = $self->hashref;
+
+  my $x = $self->setfixed;
+  return $x unless ref($x);
+  my $part_svc = $x;
+
+  if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
+    $self->usergroup(
+      [ split(',', $part_svc->part_svc_column('usergroup')->columnvalue) ] );
+  }
+
+  my $error = $self->ut_numbern('svcnum')
+              #|| $self->ut_number('domsvc')
+              || $self->ut_foreign_key('domsvc', 'svc_domain', 'svcnum' )
+              || $self->ut_textn('sec_phrase')
+  ;
+  return $error if $error;
+
+  my $ulen = $usernamemax || $self->dbdef_table->column('username')->length;
+  if ( $username_uppercase ) {
+    $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/i
+      or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
+    $recref->{username} = $1;
+  } else {
+    $recref->{username} =~ /^([a-z0-9_\-\.\&]{$usernamemin,$ulen})$/
+      or return gettext('illegal_username'). " ($usernamemin-$ulen): ". $recref->{username};
+    $recref->{username} = $1;
+  }
+
+  if ( $username_letterfirst ) {
+    $recref->{username} =~ /^[a-z]/ or return gettext('illegal_username');
+  } elsif ( $username_letter ) {
+    $recref->{username} =~ /[a-z]/ or return gettext('illegal_username');
+  }
+  if ( $username_noperiod ) {
+    $recref->{username} =~ /\./ and return gettext('illegal_username');
+  }
+  if ( $username_nounderscore ) {
+    $recref->{username} =~ /_/ and return gettext('illegal_username');
+  }
+  if ( $username_nodash ) {
+    $recref->{username} =~ /\-/ and return gettext('illegal_username');
+  }
+  unless ( $username_ampersand ) {
+    $recref->{username} =~ /\&/ and return gettext('illegal_username');
+  }
+
+  $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum: ".$recref->{popnum};
+  $recref->{popnum} = $1;
+  return "Unknown popnum" unless
+    ! $recref->{popnum} ||
+    qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
+
+  unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
+
+    $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
+    $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
+
+    $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
+    $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
+    #not all systems use gid=uid
+    #you can set a fixed gid in part_svc
+
+    return "Only root can have uid 0"
+      if $recref->{uid} == 0
+         && $recref->{username} ne 'root'
+         && $recref->{username} ne 'toor';
+
+
+    $recref->{dir} =~ /^([\/\w\-\.\&]*)$/
+      or return "Illegal directory: ". $recref->{dir};
+    $recref->{dir} = $1;
+    return "Illegal directory"
+      if $recref->{dir} =~ /(^|\/)\.+(\/|$)/; #no .. component
+    return "Illegal directory"
+      if $recref->{dir} =~ /\&/ && ! $username_ampersand;
+    unless ( $recref->{dir} ) {
+      $recref->{dir} = $dir_prefix . '/';
+      if ( $dirhash > 0 ) {
+        for my $h ( 1 .. $dirhash ) {
+          $recref->{dir} .= substr($recref->{username}, $h-1, 1). '/';
+        }
+      } elsif ( $dirhash < 0 ) {
+        for my $h ( reverse $dirhash .. -1 ) {
+          $recref->{dir} .= substr($recref->{username}, $h, 1). '/';
+        }
+      }
+      $recref->{dir} .= $recref->{username};
+    ;
+    }
+
+    unless ( $recref->{username} eq 'sync' ) {
+      if ( grep $_ eq $recref->{shell}, @shells ) {
+        $recref->{shell} = (grep $_ eq $recref->{shell}, @shells)[0];
+      } else {
+        return "Illegal shell \`". $self->shell. "\'; ".
+               $conf->dir. "/shells contains: @shells";
+      }
+    } else {
+      $recref->{shell} = '/bin/sync';
+    }
+
+  } else {
+    $recref->{gid} ne '' ? 
+      return "Can't have gid without uid" : ( $recref->{gid}='' );
+    $recref->{dir} ne '' ? 
+      return "Can't have directory without uid" : ( $recref->{dir}='' );
+    $recref->{shell} ne '' ? 
+      return "Can't have shell without uid" : ( $recref->{shell}='' );
+  }
+
+  #  $error = $self->ut_textn('finger');
+  #  return $error if $error;
+  $self->getfield('finger') =~
+    /^([\w \t\!\@\#\$\%\&\(\)\-\+\;\'\"\,\.\?\/\*\<\>]*)$/
+      or return "Illegal finger: ". $self->getfield('finger');
+  $self->setfield('finger', $1);
+
+  $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota";
+  $recref->{quota} = $1;
+
+  unless ( $part_svc->part_svc_column('slipip')->columnflag eq 'F' ) {
+    if ( $recref->{slipip} eq '' ) {
+      $recref->{slipip} = '';
+    } elsif ( $recref->{slipip} eq '0e0' ) {
+      $recref->{slipip} = '0e0';
+    } else {
+      $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
+        or return "Illegal slipip: ". $self->slipip;
+      $recref->{slipip} = $1;
+    }
+
+  }
+
+  #arbitrary RADIUS stuff; allow ut_textn for now
+  foreach ( grep /^radius_/, fields('svc_acct') ) {
+    $self->ut_textn($_);
+  }
+
+  #generate a password if it is blank
+  $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
+    unless ( $recref->{_password} );
+
+  #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
+  if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{$passwordmin,$passwordmax})$/ ) {
+    $recref->{_password} = $1.$3;
+    #uncomment this to encrypt password immediately upon entry, or run
+    #bin/crypt_pw in cron to give new users a window during which their
+    #password is available to techs, for faxing, etc.  (also be aware of 
+    #radius issues!)
+    #$recref->{password} = $1.
+    #  crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
+    #;
+  } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/\$\;\+]{13,34})$/ ) {
+    $recref->{_password} = $1.$3;
+  } elsif ( $recref->{_password} eq '*' ) {
+    $recref->{_password} = '*';
+  } elsif ( $recref->{_password} eq '!!' ) {
+    $recref->{_password} = '!!';
+  } else {
+    #return "Illegal password";
+    return gettext('illegal_password'). " $passwordmin-$passwordmax ".
+           FS::Msgcat::_gettext('illegal_password_characters').
+           ": ". $recref->{_password};
+  }
+
+  ''; #no error
+}
+
+=item radius
+
+Depriciated, use radius_reply instead.
+
+=cut
+
+sub radius {
+  carp "FS::svc_acct::radius depriciated, use radius_reply";
+  $_[0]->radius_reply;
+}
+
+=item radius_reply
+
+Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
+reply attributes of this record.
+
+Note that this is now the preferred method for reading RADIUS attributes - 
+accessing the columns directly is discouraged, as the column names are
+expected to change in the future.
+
+=cut
+
+sub radius_reply { 
+  my $self = shift;
+  my %reply =
+    map {
+      /^(radius_(.*))$/;
+      my($column, $attrib) = ($1, $2);
+      #$attrib =~ s/_/\-/g;
+      ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
+    } grep { /^radius_/ && $self->getfield($_) } fields( $self->table );
+  if ( $self->slipip && $self->slipip ne '0e0' ) {
+    $reply{$radius_ip} = $self->slipip;
+  }
+  %reply;
+}
+
+=item radius_check
+
+Returns key/value pairs, suitable for assigning to a hash, for any RADIUS
+check attributes of this record.
+
+Note that this is now the preferred method for reading RADIUS attributes - 
+accessing the columns directly is discouraged, as the column names are
+expected to change in the future.
+
+=cut
+
+sub radius_check {
+  my $self = shift;
+  my $password = $self->_password;
+  my $pw_attrib = length($password) <= 12 ? $radius_password : 'Crypt-Password';
+  ( $pw_attrib => $password,
+    map {
+      /^(rc_(.*))$/;
+      my($column, $attrib) = ($1, $2);
+      #$attrib =~ s/_/\-/g;
+      ( $FS::raddb::attrib{lc($attrib)}, $self->getfield($column) );
+    } grep { /^rc_/ && $self->getfield($_) } fields( $self->table )
+  );
+}
+
+=item domain
+
+Returns the domain associated with this account.
+
+=cut
+
+sub domain {
+  my $self = shift;
+  die "svc_acct.domsvc is null for svcnum ". $self->svcnum unless $self->domsvc;
+  my $svc_domain = $self->svc_domain
+    or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
+  $svc_domain->domain;
+}
+
+=item svc_domain
+
+Returns the FS::svc_domain record for this account's domain (see
+L<FS::svc_domain>).
+
+=cut
+
+sub svc_domain {
+  my $self = shift;
+  $self->{'_domsvc'}
+    ? $self->{'_domsvc'}
+    : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
+}
+
+=item cust_svc
+
+Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
+
+=cut
+
+sub cust_svc {
+  my $self = shift;
+  qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
+}
+
+=item email
+
+Returns an email address associated with the account.
+
+=cut
+
+sub email {
+  my $self = shift;
+  $self->username. '@'. $self->domain;
+}
+
+=item seconds_since TIMESTAMP
+
+Returns the number of seconds this account has been online since TIMESTAMP,
+according to the session monitor (see L<FS::Session>).
+
+TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
+L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+#note: POD here, implementation in FS::cust_svc
+sub seconds_since {
+  my $self = shift;
+  $self->cust_svc->seconds_since(@_);
+}
+
+=item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
+
+Returns the numbers of seconds this account has been online between
+TIMESTAMP_START (inclusive) and TIMESTAMP_END (exclusive), according to an
+external SQL radacct table, specified via sqlradius export.  Sessions which
+started in the specified range but are still open are counted from session
+start to the end of the range (unless they are over 1 day old, in which case
+they are presumed missing their stop record and not counted).  Also, sessions
+which end in therange but started earlier are counted from the start of the
+range to session end.  Finally, sessions which start before the range but end
+after are counted for the entire range.
+
+TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
+L<perlfunc/"time">.  Also see L<Time::Local> and L<Date::Parse> for conversion
+functions.
+
+=cut
+
+#note: POD here, implementation in FS::cust_svc
+sub seconds_since_sqlradacct {
+  my $self = shift;
+  $self->cust_svc->seconds_since_sqlradacct(@_);
+}
+
+=item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
+
+Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
+in this package for sessions ending between TIMESTAMP_START (inclusive) and
+TIMESTAMP_END (exclusive).
+
+TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
+L<perlfunc/"time">.  Also see L<Time::Local> and L<Date::Parse> for conversion
+functions.
+
+=cut
+
+#note: POD here, implementation in FS::cust_svc
+sub attribute_since_sqlradacct {
+  my $self = shift;
+  $self->cust_svc->attribute_since_sqlradacct(@_);
+}
+
+=item radius_groups
+
+Returns all RADIUS groups for this account (see L<FS::radius_usergroup>).
+
+=cut
+
+sub radius_groups {
+  my $self = shift;
+  if ( $self->usergroup ) {
+    #when provisioning records, export callback runs in svc_Common.pm before
+    #radius_usergroup records can be inserted...
+    @{$self->usergroup};
+  } else {
+    map { $_->groupname }
+      qsearch('radius_usergroup', { 'svcnum' => $self->svcnum } );
+  }
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item send_email
+
+This is the FS::svc_acct job-queue-able version.  It still uses
+FS::Misc::send_email under-the-hood.
+
+=cut
+
+sub send_email {
+  my %opt = @_;
+
+  eval "use FS::Misc qw(send_email)";
+  die $@ if $@;
+
+  $opt{mimetype} ||= 'text/plain';
+  $opt{mimetype} .= '; charset="iso-8859-1"' unless $opt{mimetype} =~ /charset/;
+
+  my $error = send_email(
+    'from'         => $opt{from},
+    'to'           => $opt{to},
+    'subject'      => $opt{subject},
+    'content-type' => $opt{mimetype},
+    'body'         => [ map "$_\n", split("\n", $opt{body}) ],
+  );
+  die $error if $error;
+}
+
+=item check_and_rebuild_fuzzyfiles
+
+=cut
+
+sub check_and_rebuild_fuzzyfiles {
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+  -e "$dir/svc_acct.username"
+    or &rebuild_fuzzyfiles;
+}
+
+=item rebuild_fuzzyfiles
+
+=cut
+
+sub rebuild_fuzzyfiles {
+
+  use Fcntl qw(:flock);
+
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+
+  #username
+
+  open(USERNAMELOCK,">>$dir/svc_acct.username")
+    or die "can't open $dir/svc_acct.username: $!";
+  flock(USERNAMELOCK,LOCK_EX)
+    or die "can't lock $dir/svc_acct.username: $!";
+
+  my @all_username = map $_->getfield('username'), qsearch('svc_acct', {});
+
+  open (USERNAMECACHE,">$dir/svc_acct.username.tmp")
+    or die "can't open $dir/svc_acct.username.tmp: $!";
+  print USERNAMECACHE join("\n", @all_username), "\n";
+  close USERNAMECACHE or die "can't close $dir/svc_acct.username.tmp: $!";
+
+  rename "$dir/svc_acct.username.tmp", "$dir/svc_acct.username";
+  close USERNAMELOCK;
+
+}
+
+=item all_username
+
+=cut
+
+sub all_username {
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+  open(USERNAMECACHE,"<$dir/svc_acct.username")
+    or die "can't open $dir/svc_acct.username: $!";
+  my @array = map { chomp; $_; } <USERNAMECACHE>;
+  close USERNAMECACHE;
+  \@array;
+}
+
+=item append_fuzzyfiles USERNAME
+
+=cut
+
+sub append_fuzzyfiles {
+  my $username = shift;
+
+  &check_and_rebuild_fuzzyfiles;
+
+  use Fcntl qw(:flock);
+
+  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
+
+  open(USERNAME,">>$dir/svc_acct.username")
+    or die "can't open $dir/svc_acct.username: $!";
+  flock(USERNAME,LOCK_EX)
+    or die "can't lock $dir/svc_acct.username: $!";
+
+  print USERNAME "$username\n";
+
+  flock(USERNAME,LOCK_UN)
+    or die "can't unlock $dir/svc_acct.username: $!";
+  close USERNAME;
+
+  1;
+}
+
+
+
+=item radius_usergroup_selector GROUPS_ARRAYREF [ SELECTNAME ]
+
+=cut
+
+sub radius_usergroup_selector {
+  my $sel_groups = shift;
+  my %sel_groups = map { $_=>1 } @$sel_groups;
+
+  my $selectname = shift || 'radius_usergroup';
+
+  my $dbh = dbh;
+  my $sth = $dbh->prepare(
+    'SELECT DISTINCT(groupname) FROM radius_usergroup ORDER BY groupname'
+  ) or die $dbh->errstr;
+  $sth->execute() or die $sth->errstr;
+  my @all_groups = map { $_->[0] } @{$sth->fetchall_arrayref};
+
+  my $html = <<END;
+    <SCRIPT>
+    function ${selectname}_doadd(object) {
+      var myvalue = object.${selectname}_add.value;
+      var optionName = new Option(myvalue,myvalue,false,true);
+      var length = object.$selectname.length;
+      object.$selectname.options[length] = optionName;
+      object.${selectname}_add.value = "";
+    }
+    </SCRIPT>
+    <SELECT MULTIPLE NAME="$selectname">
+END
+
+  foreach my $group ( @all_groups ) {
+    $html .= '<OPTION';
+    if ( $sel_groups{$group} ) {
+      $html .= ' SELECTED';
+      $sel_groups{$group} = 0;
+    }
+    $html .= ">$group</OPTION>\n";
+  }
+  foreach my $group ( grep { $sel_groups{$_} } keys %sel_groups ) {
+    $html .= "<OPTION SELECTED>$group</OPTION>\n";
+  };
+  $html .= '</SELECT>';
+
+  $html .= qq!<BR><INPUT TYPE="text" NAME="${selectname}_add">!.
+           qq!<INPUT TYPE="button" VALUE="Add new group" onClick="${selectname}_doadd(this.form)">!;
+
+  $html;
+}
+
+=back
+
+=head1 BUGS
+
+The $recref stuff in sub check should be cleaned up.
+
+The suspend, unsuspend and cancel methods update the database, but not the
+current object.  This is probably a bug as it's unexpected and
+counterintuitive.
+
+radius_usergroup_selector?  putting web ui components in here?  they should
+probably live somewhere else...
+
+=head1 SEE ALSO
+
+L<FS::svc_Common>, edit/part_svc.cgi from an installed web interface,
+export.html from the base documentation, L<FS::Record>, L<FS::Conf>,
+L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, L<FS::queue>,
+L<freeside-queued>), L<FS::svc_acct_pop>,
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_acct_pop.pm b/FS/FS/svc_acct_pop.pm
new file mode 100644 (file)
index 0000000..196ab7e
--- /dev/null
@@ -0,0 +1,209 @@
+package FS::svc_acct_pop;
+
+use strict;
+use vars qw( @ISA @EXPORT_OK @svc_acct_pop %svc_acct_pop );
+use FS::Record qw( qsearch qsearchs );
+
+@ISA = qw( FS::Record Exporter );
+@EXPORT_OK = qw( popselector );
+
+=head1 NAME
+
+FS::svc_acct_pop - Object methods for svc_acct_pop records
+
+=head1 SYNOPSIS
+
+  use FS::svc_acct_pop;
+
+  $record = new FS::svc_acct_pop \%hash;
+  $record = new FS::svc_acct_pop { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $html = FS::svc_acct_pop::popselector( $popnum, $state );
+
+=head1 DESCRIPTION
+
+An FS::svc_acct object represents an point of presence.  FS::svc_acct_pop
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item popnum - primary key (assigned automatically for new accounts)
+
+=item city
+
+=item state
+
+=item ac - area code
+
+=item exch - exchange
+
+=item loc - rest of number
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new point of presence (if only it were that easy!).  To add the 
+point of presence to the database, see L<"insert">.
+
+=cut
+
+sub table { 'svc_acct_pop'; }
+
+=item insert
+
+Adds this point of presence to the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item delete
+
+Removes this point of presence from the database.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid point of presence.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+    $self->ut_numbern('popnum')
+      or $self->ut_text('city')
+      or $self->ut_text('state')
+      or $self->ut_number('ac')
+      or $self->ut_number('exch')
+      or $self->ut_numbern('loc')
+  ;
+
+}
+
+=item text
+
+Returns:
+
+"$city, $state ($ac)/$exch"
+
+=cut
+
+sub text {
+  my $self = shift;
+  $self->city. ', '. $self->state.
+    ' ('. $self->ac. ')/'. $self->exch. '-'. $self->loc;
+}
+
+=back
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item popselector [ POPNUM [ STATE ] ]
+
+=cut
+
+#horrible false laziness with signup.cgi (pull special-case for 0 & 1
+# pop code out from signup.cgi??)
+sub popselector {
+  my( $popnum, $state ) = @_;
+
+  unless ( @svc_acct_pop ) { #cache pop list
+    @svc_acct_pop = qsearch('svc_acct_pop', {} );
+    %svc_acct_pop = ();
+    push @{$svc_acct_pop{$_->state}}, $_ foreach @svc_acct_pop;
+  }
+
+  my $text = <<END;
+    <SCRIPT>
+    function opt(what,href,text) {
+      var optionName = new Option(text, href, false, false)
+      var length = what.length;
+      what.options[length] = optionName;
+    }
+    
+    function popstate_changed(what) {
+      state = what.options[what.selectedIndex].text;
+      what.form.popnum.options.length = 0
+      what.form.popnum.options[0] = new Option("", "", false, true);
+END
+
+  foreach my $popstate ( sort { $a cmp $b } keys %svc_acct_pop ) {
+    $text .= "\nif ( state == \"$popstate\" ) {\n";
+
+    foreach my $pop ( @{$svc_acct_pop{$popstate}}) {
+      my $o_popnum = $pop->popnum;
+      my $poptext = $pop->text;
+      $text .= "opt(what.form.popnum, \"$o_popnum\", \"$poptext\");\n"
+    }
+    $text .= "}\n";
+  }
+
+  $text .= "}\n</SCRIPT>\n";
+
+  $text .=
+    qq!<SELECT NAME="popstate" SIZE=1 onChange="popstate_changed(this)">!.
+    qq!<OPTION> !;
+  $text .= "<OPTION>$_" foreach sort { $a cmp $b } keys %svc_acct_pop;
+  $text .= '</SELECT>'; #callback? return 3 html pieces?  #'</TD><TD>';
+
+  $text .= qq!<SELECT NAME="popnum" SIZE=1><OPTION> !;
+  my @initial_select;
+  if ( scalar(@svc_acct_pop) > 100 ) {
+    @initial_select = qsearchs( 'svc_acct_pop', { 'popnum' => $popnum } );
+  } else {
+    @initial_select = @svc_acct_pop;
+  }
+  foreach my $pop ( @initial_select ) {
+    $text .= qq!<OPTION VALUE="!. $pop->popnum. '"'.
+             ( ( $popnum && $pop->popnum == $popnum ) ? ' SELECTED' : '' ). ">".
+             $pop->text;
+  }
+  $text .= '</SELECT>';
+
+  $text;
+
+}
+
+=back
+
+=head1 VERSION
+
+$Id: svc_acct_pop.pm,v 1.9 2003-07-04 01:37:46 ivan Exp $
+
+=head1 BUGS
+
+It should be renamed to part_pop.
+
+popselector?  putting web ui components in here?  they should probably live
+somewhere else...  
+
+popselector: pull special-case for 0 & 1 pop code out from signup.cgi
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::svc_acct>, L<FS::part_pop_local>, schema.html from the
+base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm
new file mode 100755 (executable)
index 0000000..45f6c36
--- /dev/null
@@ -0,0 +1,288 @@
+package FS::svc_broadband;
+
+use strict;
+use vars qw(@ISA $conf);
+use FS::Record qw( qsearchs qsearch dbh );
+use FS::svc_Common;
+use FS::cust_svc;
+use FS::addr_block;
+use NetAddr::IP;
+
+@ISA = qw( FS::svc_Common );
+
+$FS::UID::callback{'FS::svc_broadband'} = sub { 
+  $conf = new FS::Conf;
+};
+
+=head1 NAME
+
+FS::svc_broadband - Object methods for svc_broadband records
+
+=head1 SYNOPSIS
+
+  use FS::svc_broadband;
+
+  $record = new FS::svc_broadband \%hash;
+  $record = new FS::svc_broadband { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->suspend;
+
+  $error = $record->unsuspend;
+
+  $error = $record->cancel;
+
+=head1 DESCRIPTION
+
+An FS::svc_broadband object represents a 'broadband' Internet connection, such
+as a DSL, cable modem, or fixed wireless link.  These services are assumed to
+have the following properties:
+
+FS::svc_broadband inherits from FS::svc_Common.  The following fields are
+currently supported:
+
+=over 4
+
+=item svcnum - primary key
+
+=item blocknum - see FS::addr_block
+
+=item
+speed_up - maximum upload speed, in bits per second.  If set to zero, upload
+speed will be unlimited.  Exports that do traffic shaping should handle this
+correctly, and not blindly set the upload speed to zero and kill the customer's
+connection.
+
+=item
+speed_down - maximum download speed, as above
+
+=item ip_addr - the customer's IP address.  If the customer needs more than one
+IP address, set this to the address of the customer's router.  As a result, the
+customer's router will have the same address for both its internal and external
+interfaces thus saving address space.  This has been found to work on most NAT
+routers available.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new svc_broadband.  To add the record to the database, see
+"insert".
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'svc_broadband'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
+defined.  An FS::cust_svc record will be created and inserted.
+
+=cut
+
+# Standard FS::svc_Common::insert
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# Standard FS::svc_Common::delete
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# Standard FS::svc_Common::replace
+
+=item sb_field
+
+Returns a list of FS::sb_field objects assigned to this object.
+
+=cut
+
+sub sb_field {
+  my $self = shift;
+
+  return qsearch( 'sb_field', { svcnum => $self->svcnum } );
+}
+
+=item sb_field_hashref
+
+Returns a hashref of the FS::sb_field key/value pairs for this object.
+
+Deprecated.  Please don't use it.
+
+=cut
+
+# Kristian wrote this, but don't hold it against him.  He was under a powerful
+# distracting influence whom he evidently found much more interesting than
+# svc_broadband.pm.  I can't say I blame him.
+
+sub sb_field_hashref {
+  my $self = shift;
+  my $svcpart = shift;
+
+  if ((not $svcpart) && ($self->cust_svc)) {
+    $svcpart = $self->cust_svc->svcpart;
+  }
+
+  my $hashref = {};
+
+  map {
+    my $sb_field = qsearchs('sb_field', { sbfieldpart => $_->sbfieldpart,
+                                          svcnum => $self->svcnum });
+    $hashref->{$_->getfield('name')} = $sb_field ? $sb_field->getfield('value') : '';
+  } qsearch('part_sb_field', { svcpart => $svcpart });
+
+  return $hashref;
+
+}
+
+=item suspend
+
+Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
+
+=item unsuspend
+
+Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
+
+=item cancel
+
+Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
+
+=item check
+
+Checks all fields to make sure this is a valid broadband service.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+  my $x = $self->setfixed;
+
+  return $x unless ref($x);
+
+  my $error =
+    $self->ut_numbern('svcnum')
+    || $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum')
+    || $self->ut_number('speed_up')
+    || $self->ut_number('speed_down')
+    || $self->ut_ipn('ip_addr')
+  ;
+  return $error if $error;
+
+  if($self->speed_up < 0) { return 'speed_up must be positive'; }
+  if($self->speed_down < 0) { return 'speed_down must be positive'; }
+
+  if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
+    $self->ip_addr($self->addr_block->next_free_addr->addr);
+    if (not $self->ip_addr) {
+      return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
+    }
+  }
+
+  # This should catch errors in the ip_addr.  If it doesn't,
+  # they'll almost certainly not map into the block anyway.
+  my $self_addr = $self->NetAddr; #netmask is /32
+  return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
+
+  my $block_addr = $self->addr_block->NetAddr;
+  unless ($block_addr->contains($self_addr)) {
+    return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
+  }
+
+  my $router = $self->addr_block->router 
+    or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
+  if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
+  } # do nothing
+  else {
+    return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
+  }
+
+
+  ''; #no error
+}
+
+=item NetAddr
+
+Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
+is /32.
+
+=cut
+
+sub NetAddr {
+  my $self = shift;
+  return new NetAddr::IP ($self->ip_addr);
+}
+
+=item addr_block
+
+Returns the FS::addr_block record (i.e. the address block) for this broadband service.
+
+=cut
+
+sub addr_block {
+  my $self = shift;
+
+  return qsearchs('addr_block', { blocknum => $self->blocknum });
+}
+
+=back
+
+=item allowed_routers
+
+Returns a list of allowed FS::router objects.
+
+=cut
+
+sub allowed_routers {
+  my $self = shift;
+
+  return map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
+}
+
+=head1 BUGS
+
+I think there's one place in the code where we actually use sb_field_hashref.
+That's a bug in itself.
+
+The real problem with it is that we're still grappling with the question of how
+tightly xfields should be integrated with real fields.  There are a few
+different directions we could go with it--we I<could> override several
+functions in Record so that xfields behave almost exactly like real fields (can
+be set with setfield(), appear in fields() and hash(), used as criteria in
+qsearch(), etc.).
+
+=head1 SEE ALSO
+
+FS::svc_Common, FS::Record, FS::addr_block, FS::sb_field,
+FS::part_svc, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_domain.pm b/FS/FS/svc_domain.pm
new file mode 100644 (file)
index 0000000..32b9456
--- /dev/null
@@ -0,0 +1,434 @@
+package FS::svc_domain;
+
+use strict;
+use vars qw( @ISA $whois_hack $conf
+  @defaultrecords $soadefaultttl $soaemail $soaexpire $soamachine
+  $soarefresh $soaretry
+);
+use Carp;
+use Date::Format;
+use Net::Whois 1.0;
+use FS::Record qw(fields qsearch qsearchs dbh);
+use FS::Conf;
+use FS::svc_Common;
+use FS::cust_svc;
+use FS::svc_acct;
+use FS::cust_pkg;
+use FS::cust_main;
+use FS::domain_record;
+use FS::queue;
+
+@ISA = qw( FS::svc_Common );
+
+#ask FS::UID to run this stuff for us later
+$FS::UID::callback{'FS::domain'} = sub { 
+  $conf = new FS::Conf;
+
+  @defaultrecords = $conf->config('defaultrecords');
+  $soadefaultttl = $conf->config('soadefaultttl');
+  $soaemail      = $conf->config('soaemail');
+  $soaexpire     = $conf->config('soaexpire');
+  $soamachine    = $conf->config('soamachine');
+  $soarefresh    = $conf->config('soarefresh');
+  $soaretry      = $conf->config('soaretry');
+
+};
+
+=head1 NAME
+
+FS::svc_domain - Object methods for svc_domain records
+
+=head1 SYNOPSIS
+
+  use FS::svc_domain;
+
+  $record = new FS::svc_domain \%hash;
+  $record = new FS::svc_domain { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->suspend;
+
+  $error = $record->unsuspend;
+
+  $error = $record->cancel;
+
+=head1 DESCRIPTION
+
+An FS::svc_domain object represents a domain.  FS::svc_domain inherits from
+FS::svc_Common.  The following fields are currently supported:
+
+=over 4
+
+=item svcnum - primary key (assigned automatically for new accounts)
+
+=item domain
+
+=item catchall - optional svcnum of an svc_acct record, designating an email catchall account.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new domain.  To add the domain to the database, see L<"insert">.
+
+=cut
+
+sub table { 'svc_domain'; }
+
+=item insert
+
+Adds this domain to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+The additional fields I<pkgnum> and I<svcpart> (see L<FS::cust_svc>) should be 
+defined.  An FS::cust_svc record will be created and inserted.
+
+The additional field I<action> should be set to I<N> for new domains or I<M>
+for transfers.
+
+A registration or transfer email will be submitted unless
+$FS::svc_domain::whois_hack is true.
+
+The additional field I<email> can be used to manually set the admin contact
+email address on this email.  Otherwise, the svc_acct records for this package 
+(see L<FS::cust_pkg>) are searched.  If there is exactly one svc_acct record
+in the same package, it is automatically used.  Otherwise an error is returned.
+
+If any I<soamachine> configuration file exists, an SOA record is added to
+the domain_record table (see <FS::domain_record>).
+
+If any records are defined in the I<defaultrecords> configuration file,
+appropriate records are added to the domain_record table (see
+L<FS::domain_record>).
+
+=cut
+
+sub insert {
+  my $self = shift;
+  my $error;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $error = $self->check;
+  return $error if $error;
+
+  return "Domain in use (here)"
+    if qsearchs( 'svc_domain', { 'domain' => $self->domain } );
+
+  my $whois = $self->whois;
+  if ( $self->action eq "N" && ! $whois_hack && $whois ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Domain in use (see whois)";
+  }
+  if ( $self->action eq "M" && ! $whois ) {
+    $dbh->rollback if $oldAutoCommit;
+    return "Domain not found (see whois)";
+  }
+
+  $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $self->submit_internic unless $whois_hack;
+
+  if ( $soamachine ) {
+    my $soa = new FS::domain_record {
+      'svcnum'  => $self->svcnum,
+      'reczone' => '@',
+      'recaf'   => 'IN',
+      'rectype' => 'SOA',
+      'recdata' => "$soamachine $soaemail ( ". time2str("%Y%m%d", time). "00 ".
+                   "$soarefresh $soaretry $soaexpire $soadefaultttl )"
+    };
+    $error = $soa->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "couldn't insert SOA record for new domain: $error";
+    }
+
+    foreach my $record ( @defaultrecords ) {
+      my($zone,$af,$type,$data) = split(/\s+/,$record,4);
+      my $domain_record = new FS::domain_record {
+        'svcnum'  => $self->svcnum,
+        'reczone' => $zone,
+        'recaf'   => $af,
+        'rectype' => $type,
+        'recdata' => $data,
+      };
+      my $error = $domain_record->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "couldn't insert record for new domain: $error";
+      }
+    }
+
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  ''; #no error
+}
+
+=item delete
+
+Deletes this domain from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+The corresponding FS::cust_svc record will be deleted as well.
+
+=cut
+
+sub delete {
+  my $self = shift;
+
+  return "Can't delete a domain which has accounts!"
+    if qsearch( 'svc_acct', { 'domsvc' => $self->svcnum } );
+
+  #return "Can't delete a domain with (domain_record) zone entries!"
+  #  if qsearch('domain_record', { 'svcnum' => $self->svcnum } );
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $domain_record ( reverse $self->domain_record ) {
+    my $error = $domain_record->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+}
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+
+  return "Can't change domain - reorder."
+    if $old->getfield('domain') ne $new->getfield('domain'); 
+
+  my $error = $new->SUPER::replace($old);
+  return $error if $error;
+}
+
+=item suspend
+
+Just returns false (no error) for now.
+
+Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item unsuspend
+
+Just returns false (no error) for now.
+
+Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item cancel
+
+Just returns false (no error) for now.
+
+Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item check
+
+Checks all fields to make sure this is a valid domain.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+Sets any fixed values; see L<FS::part_svc>.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $x = $self->setfixed;
+  return $x unless ref($x);
+  #my $part_svc = $x;
+
+  my $error = $self->ut_numbern('svcnum')
+              || $self->ut_numbern('catchall')
+  ;
+  return $error if $error;
+
+  #hmm
+  my $pkgnum;
+  if ( $self->svcnum ) {
+    my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $self->svcnum } );
+    $pkgnum = $cust_svc->pkgnum;
+  } else {
+    $pkgnum = $self->pkgnum;
+  }
+
+  my($recref) = $self->hashref;
+
+  unless ( $whois_hack ) {
+    unless ( $self->email ) { #find out an email address
+      my @svc_acct;
+      foreach ( qsearch( 'cust_svc', { 'pkgnum' => $pkgnum } ) ) {
+        my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $_->svcnum } );
+        push @svc_acct, $svc_acct if $svc_acct;
+      }
+
+      if ( scalar(@svc_acct) == 0 ) {
+        return "Must order an account in package ". $pkgnum. " first";
+      } elsif ( scalar(@svc_acct) > 1 ) {
+        return "More than one account in package ". $pkgnum. ": specify admin contact email";
+      } else {
+        $self->email($svc_acct[0]->email );
+      }
+    }
+  }
+
+  #if ( $recref->{domain} =~ /^([\w\-\.]{1,22})\.(com|net|org|edu)$/ ) {
+  if ( $recref->{domain} =~ /^([\w\-]{1,63})\.(com|net|org|edu)$/ ) {
+    $recref->{domain} = "$1.$2";
+  # hmmmmmmmm.
+  } elsif ( $whois_hack && $recref->{domain} =~ /^([\w\-\.]+)$/ ) {
+    $recref->{domain} = $1;
+  } else {
+    return "Illegal domain ". $recref->{domain}.
+           " (or unknown registry - try \$whois_hack)";
+  }
+
+  $recref->{action} =~ /^(M|N)$/ or return "Illegal action";
+  $recref->{action} = $1;
+
+  if ( $recref->{catchall} ne '' ) {
+    my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $recref->{catchall} } );
+    return "Unknown catchall" unless $svc_acct;
+  }
+
+  $self->ut_textn('purpose');
+
+}
+
+=item domain_record
+
+=cut
+
+sub domain_record {
+  my $self = shift;
+
+  my %order = (
+    SOA => 1,
+    NS => 2,
+    MX => 3,
+    CNAME => 4,
+    A => 5,
+  );
+
+  sort { $order{$a->rectype} <=> $order{$b->rectype} }
+    qsearch('domain_record', { svcnum => $self->svcnum } );
+
+}
+
+sub catchall_svc_acct {
+  my $self = shift;
+  if ( $self->catchall ) {
+    qsearchs( 'svc_acct', { 'svcnum' => $self->catchall } );
+  } else {
+    '';
+  }
+}
+
+=item whois
+
+Returns the Net::Whois::Domain object (see L<Net::Whois>) for this domain, or
+undef if the domain is not found in whois.
+
+(If $FS::svc_domain::whois_hack is true, returns that in all cases instead.)
+
+=cut
+
+sub whois {
+  $whois_hack or new Net::Whois::Domain $_[0]->domain;
+}
+
+=item _whois
+
+Depriciated.
+
+=cut
+
+sub _whois {
+  die "_whois depriciated";
+}
+
+=item submit_internic
+
+Submits a registration email for this domain.
+
+=cut
+
+sub submit_internic {
+  #my $self = shift;
+  carp "submit_internic depreciated";
+}
+
+=back
+
+=head1 BUGS
+
+Delete doesn't send a registration template.
+
+All registries should be supported.
+
+Should change action to a real field.
+
+The $recref stuff in sub check should be cleaned up.
+
+=head1 SEE ALSO
+
+L<FS::svc_Common>, L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>,
+L<FS::part_svc>, L<FS::cust_pkg>, L<Net::Whois>, schema.html from the base
+documentation, config.html from the base documentation.
+
+=cut
+
+1;
+
+
diff --git a/FS/FS/svc_forward.pm b/FS/FS/svc_forward.pm
new file mode 100644 (file)
index 0000000..2b1fb92
--- /dev/null
@@ -0,0 +1,282 @@
+package FS::svc_forward;
+
+use strict;
+use vars qw( @ISA );
+use FS::Conf;
+use FS::Record qw( fields qsearch qsearchs dbh );
+use FS::svc_Common;
+use FS::cust_svc;
+use FS::svc_acct;
+use FS::svc_domain;
+
+@ISA = qw( FS::svc_Common );
+
+=head1 NAME
+
+FS::svc_forward - Object methods for svc_forward records
+
+=head1 SYNOPSIS
+
+  use FS::svc_forward;
+
+  $record = new FS::svc_forward \%hash;
+  $record = new FS::svc_forward { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->suspend;
+
+  $error = $record->unsuspend;
+
+  $error = $record->cancel;
+
+=head1 DESCRIPTION
+
+An FS::svc_forward object represents a mail forwarding alias.  FS::svc_forward
+inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item svcnum - primary key (assigned automatcially for new accounts)
+
+=item srcsvc - svcnum of the source of the forward (see L<FS::svc_acct>)
+
+=item dstsvc - svcnum of the destination of the forward (see L<FS::svc_acct>)
+
+=item dst - foreign destination (email address) - forward not local to freeside
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new mail forwarding alias.  To add the mail forwarding alias to the
+database, see L<"insert">.
+
+=cut
+
+sub table { 'svc_forward'; }
+
+=item insert
+
+Adds this mail forwarding alias to the database.  If there is an error, returns
+the error, otherwise returns false.
+
+The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
+defined.  An FS::cust_svc record will be created and inserted.
+
+=cut
+
+sub insert {
+  my $self = shift;
+  my $error;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  $error = $self->check;
+  return $error if $error;
+
+  $error = $self->SUPER::insert;
+  if ($error) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  ''; #no error
+
+}
+
+=item delete
+
+Deletes this mail forwarding alias from the database.  If there is an error,
+returns the error, otherwise returns false.
+
+The corresponding FS::cust_svc record will be deleted as well.
+
+=cut
+
+sub delete {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::Autocommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+}
+
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+
+  if ( $new->srcsvc != $old->srcsvc
+       && ( $new->dstsvc != $old->dstsvc
+            || ! $new->dstsvc && $new->dst ne $old->dst 
+          )
+      ) {
+    return "Can't change both source and destination of a mail forward!"
+  }
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $new->SUPER::replace($old);
+  if ($error) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+}
+
+=item suspend
+
+Just returns false (no error) for now.
+
+Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item unsuspend
+
+Just returns false (no error) for now.
+
+Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item cancel
+
+Just returns false (no error) for now.
+
+Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item check
+
+Checks all fields to make sure this is a valid mail forwarding alias.  If there
+is an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+Sets any fixed values; see L<FS::part_svc>.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $x = $self->setfixed;
+  return $x unless ref($x);
+  #my $part_svc = $x;
+
+  my $error = $self->ut_numbern('svcnum')
+              || $self->ut_number('srcsvc')
+              || $self->ut_numbern('dstsvc')
+  ;
+  return $error if $error;
+
+  return "Unknown srcsvc" unless $self->srcsvc_acct;
+
+  return "Both dstsvc and dst were defined; only one can be specified"
+    if $self->dstsvc && $self->dst;
+
+  return "one of dstsvc or dst is required"
+    unless $self->dstsvc || $self->dst;
+
+  #return "Unknown dstsvc: $dstsvc" unless $self->dstsvc_acct || ! $self->dstsvc;
+  return "Unknown dstsvc"
+    unless qsearchs('svc_acct', { 'svcnum' => $self->dstsvc } )
+           || ! $self->dstsvc;
+
+
+  if ( $self->dst ) {
+    $self->dst =~ /^([\w\.\-]+)\@(([\w\-]+\.)+\w+)$/
+       or return "Illegal dst: ". $self->dst;
+    $self->dst("$1\@$2");
+  } else {
+    $self->dst('');
+  }
+
+  ''; #no error
+}
+
+=item srcsvc_acct
+
+Returns the FS::svc_acct object referenced by the srcsvc column.
+
+=cut
+
+sub srcsvc_acct {
+  my $self = shift;
+  qsearchs('svc_acct', { 'svcnum' => $self->srcsvc } );
+}
+
+=item dstsvc_acct
+
+Returns the FS::svc_acct object referenced by the srcsvc column, or false for
+forwards not local to freeside.
+
+=cut
+
+sub dstsvc_acct {
+  my $self = shift;
+  qsearchs('svc_acct', { 'svcnum' => $self->dstsvc } );
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>,
+L<FS::svc_acct>, L<FS::svc_domain>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_www.pm b/FS/FS/svc_www.pm
new file mode 100644 (file)
index 0000000..d7a42c8
--- /dev/null
@@ -0,0 +1,276 @@
+package FS::svc_www;
+
+use strict;
+use vars qw(@ISA $conf $apacheip);
+#use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearchs dbh );
+use FS::svc_Common;
+use FS::cust_svc;
+use FS::domain_record;
+use FS::svc_acct;
+use FS::svc_domain;
+
+@ISA = qw( FS::svc_Common );
+
+#ask FS::UID to run this stuff for us later
+$FS::UID::callback{'FS::svc_www'} = sub { 
+  $conf = new FS::Conf;
+  $apacheip = $conf->config('apacheip');
+};
+
+=head1 NAME
+
+FS::svc_www - Object methods for svc_www records
+
+=head1 SYNOPSIS
+
+  use FS::svc_www;
+
+  $record = new FS::svc_www \%hash;
+  $record = new FS::svc_www { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->suspend;
+
+  $error = $record->unsuspend;
+
+  $error = $record->cancel;
+
+=head1 DESCRIPTION
+
+An FS::svc_www object represents an web virtual host.  FS::svc_www inherits
+from FS::svc_Common.  The following fields are currently supported:
+
+=over 4
+
+=item svcnum - primary key
+
+=item recnum - DNS `A' record corresponding to this web virtual host. (see L<FS::domain_record>)
+
+=item usersvc - account (see L<FS::svc_acct>) corresponding to this web virtual host.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new web virtual host.  To add the record to the database, see
+L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'svc_www'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
+defined.  An FS::cust_svc record will be created and inserted.
+
+=cut
+
+sub insert {
+  my $self = shift;
+
+  my $error = $self->check;
+  return $error if $error;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  #if ( $self->recnum =~ /^([\w\-]+|\@)\.(([\w\.\-]+\.)+\w+)$/ ) {
+  if ( $self->recnum =~ /^([\w\-]+|\@)\.(\d+)$/ ) {
+    my( $reczone, $domain_svcnum ) = ( $1, $2 );
+    unless ( $apacheip ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Configuration option apacheip not set; can't autocreate A record";
+             #"for $reczone". $svc_domain->domain;
+    }
+    my $domain_record = new FS::domain_record {
+      'svcnum'  => $domain_svcnum,
+      'reczone' => $reczone,
+      'recaf'   => 'IN',
+      'rectype' => 'A',
+      'recdata' => $apacheip,
+    };
+    $error = $domain_record->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+    $self->recnum($domain_record->recnum);
+  }
+
+  $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  my $error;
+
+  $error = $self->SUPER::delete;
+  return $error if $error;
+
+  '';
+}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+  my $error;
+
+  $error = $new->SUPER::replace($old);
+  return $error if $error;
+
+  '';
+}
+
+=item suspend
+
+Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item unsuspend
+
+Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item cancel
+
+Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item check
+
+Checks all fields to make sure this is a valid web virtual host.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and repalce methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $x = $self->setfixed;
+  return $x unless ref($x);
+  #my $part_svc = $x;
+
+  my $error =
+    $self->ut_numbern('svcnum')
+#    || $self->ut_number('recnum')
+    || $self->ut_number('usersvc')
+  ;
+  return $error if $error;
+
+  if ( $self->recnum =~ /^(\d+)$/ ) {
+  
+    $self->recnum($1);
+    return "Unknown recnum: ". $self->recnum
+      unless qsearchs('domain_record', { 'recnum' => $self->recnum } );
+
+  } elsif ( $self->recnum =~ /^([\w\-]+|\@)\.(([\w\.\-]+\.)+\w+)$/ ) {
+
+    my( $reczone, $domain ) = ( $1, $2 );
+
+    my $svc_domain = qsearchs( 'svc_domain', { 'domain' => $domain } )
+      or return "unknown domain $domain (recnum $1.$2)";
+
+    my $domain_record = qsearchs( 'domain_record', {
+      'reczone' => $reczone,
+      'svcnum' => $svc_domain->svcnum,
+    });
+
+    if ( $domain_record ) {
+      $self->recnum($domain_record->recnum);
+    } else {
+      #insert will create it
+      #$self->recnum("$reczone.$domain");
+      $self->recnum("$reczone.". $svc_domain->svcnum);
+    }
+
+  } else {
+    return "Illegal recnum: ". $self->recnum;
+  }
+
+  return "Unknown usersvc (svc_acct.svcnum): ". $self->usersvc
+    unless qsearchs('svc_acct', { 'svcnum' => $self->usersvc } );
+
+  ''; #no error
+}
+
+=item domain_record
+
+Returns the FS::domain_record record for this web virtual host's zone (see
+L<FS::domain_record>).
+
+=cut
+
+sub domain_record {
+  my $self = shift;
+  qsearchs('domain_record', { 'recnum' => $self->recnum } );
+}
+
+=item svc_acct
+
+Returns the FS::svc_acct record for this web virtual host's owner (see
+L<FS::svc_acct>).
+
+=cut
+
+sub svc_acct {
+  my $self = shift;
+  qsearchs('svc_acct', { 'svcnum' => $self->usersvc } );
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_Common>, L<FS::Record>, L<FS::domain_record>, L<FS::cust_svc>,
+L<FS::part_svc>, L<FS::cust_pkg>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/type_pkgs.pm b/FS/FS/type_pkgs.pm
new file mode 100644 (file)
index 0000000..efba60d
--- /dev/null
@@ -0,0 +1,126 @@
+package FS::type_pkgs;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::agent_type;
+use FS::part_pkg;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::type_pkgs - Object methods for type_pkgs records
+
+=head1 SYNOPSIS
+
+  use FS::type_pkgs;
+
+  $record = new FS::type_pkgs \%hash;
+  $record = new FS::type_pkgs { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::type_pkgs record links an agent type (see L<FS::agent_type>) to a
+billing item definition (see L<FS::part_pkg>).  FS::type_pkgs inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item typenum - Agent type, see L<FS::agent_type>
+
+=item pkgpart - Billing item definition, see L<FS::part_pkg>
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'type_pkgs'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is an error,
+returns the error, otherwise returns false.  Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_number('typenum')
+    || $self->ut_number('pkgpart')
+  ;
+  return $error if $error;
+
+  return "Unknown typenum"
+    unless qsearchs( 'agent_type', { 'typenum' => $self->typenum } );
+
+  return "Unknown pkgpart"
+    unless qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
+
+  ''; #no error
+}
+
+=item part_pkg
+
+Returns the FS::part_pkg object associated with this record.
+
+=cut
+
+sub part_pkg {
+  my $self = shift;
+  qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
+}
+
+=cut
+
+=back
+
+=head1 VERSION
+
+$Id: type_pkgs.pm,v 1.2 2002-10-04 12:57:06 ivan Exp $
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::agent_type>, L<FS::part_pkgs>, schema.html from the base
+documentation.
+
+=cut
+
+1;
+
diff --git a/FS/MANIFEST b/FS/MANIFEST
new file mode 100644 (file)
index 0000000..846f373
--- /dev/null
@@ -0,0 +1,197 @@
+Changes
+MANIFEST
+MANIFEST.SKIP
+Makefile.PL
+README
+bin/freeside-addoutsource
+bin/freeside-addoutsourceuser
+bin/freeside-adduser
+bin/freeside-apply-credits
+bin/freeside-bill
+bin/freeside-cc-receipts-report
+bin/freeside-count-active-customers
+bin/freeside-credit-report
+bin/freeside-daily
+bin/freeside-deloutsource
+bin/freeside-deloutsourceuser
+bin/freeside-deluser
+bin/freeside-email
+bin/freeside-expiration-alerter
+bin/freeside-queued
+bin/freeside-radgroup
+bin/freeside-receivables-report
+bin/freeside-reexport
+bin/freeside-selfservice-server
+bin/freeside-setinvoice
+bin/freeside-setup
+bin/freeside-sqlradius-radacctd
+bin/freeside-sqlradius-reset
+bin/freeside-sqlradius-seconds
+bin/freeside-tax-report
+FS.pm
+FS/CGI.pm
+FS/InitHandler.pm
+FS/ClientAPI.pm
+FS/ClientAPI/passwd.pm
+FS/ClientAPI/MyAccount.pm
+FS/Conf.pm
+FS/ConfItem.pm
+FS/Misc.pm
+FS/Record.pm
+FS/SearchCache.pm
+FS/UI/Base.pm
+FS/UI/CGI.pm
+FS/UI/Gtk.pm
+FS/UI/agent.pm
+FS/UID.pm
+FS/Msgcat.pm
+FS/agent.pm
+FS/agent_type.pm
+FS/cust_bill.pm
+FS/cust_bill_pkg.pm
+FS/cust_bill_pkg_detail.pm
+FS/cust_credit.pm
+FS/cust_credit_bill.pm
+FS/cust_main.pm
+FS/cust_main_county.pm
+FS/cust_main_invoice.pm
+FS/cust_pay.pm
+FS/cust_bill_event.pm
+FS/cust_bill_pay.pm
+FS/cust_pay_batch.pm
+FS/cust_pkg.pm
+FS/cust_refund.pm
+FS/cust_credit_refund.pm
+FS/cust_svc.pm
+FS/part_bill_event.pm
+FS/export_svc.pm
+FS/part_export.pm
+FS/part_export_option.pm
+FS/part_export/apache.pm
+FS/part_export/bind.pm
+FS/part_export/bind_slave.pm
+FS/part_export/bsdshell.pm
+FS/part_export/cp.pm
+FS/part_export/cyrus.pm
+FS/part_export/domain_shellcommands.pm
+FS/part_export/forward_shellcommands.pm
+FS/part_export/http.pm
+FS/part_export/infostreet.pm
+FS/part_export/ldap.pm
+FS/part_export/null.pm
+FS/part_export/shellcommands.pm
+FS/part_export/shellcommands_withdomain.pm
+FS/part_export/sqlmail.pm
+FS/part_export/sqlradius.pm
+FS/part_export/sysvshell.pm
+FS/part_export/textradius.pm
+FS/part_export/vpopmail.pm
+FS/part_export/www_shellcommands.pm
+FS/part_pkg.pm
+FS/part_pop_local.pm
+FS/part_referral.pm
+FS/part_svc.pm
+FS/part_svc_column.pm
+FS/part_router_field.pm
+FS/part_sb_field.pm
+FS/part_svc_router.pm
+FS/pkg_svc.pm
+FS/svc_Common.pm
+FS/svc_acct.pm
+FS/svc_acct_pop.pm
+FS/svc_broadband.pm
+FS/svc_domain.pm
+FS/router.pm
+FS/router_field.pm
+FS/type_pkgs.pm
+FS/nas.pm
+FS/port.pm
+FS/session.pm
+FS/domain_record.pm
+FS/prepay_credit.pm
+FS/svc_www.pm
+FS/svc_forward.pm
+FS/sb_field.pm
+FS/raddb.pm
+FS/radius_usergroup.pm
+FS/queue.pm
+FS/queue_arg.pm
+FS/queue_depend.pm
+FS/msgcat.pm
+FS/cust_tax_exempt.pm
+t/agent.t
+t/agent_type.t
+t/CGI.t
+t/InitHandler.t
+t/ClientAPI.t
+t/Conf.t
+t/ConfItem.t
+t/Misc.t
+t/Record.t
+t/UID.t
+t/Msgcat.t
+t/SearchCache.t
+t/cust_bill.t
+t/cust_bill_event.t
+t/cust_bill_pay.t
+t/cust_bill_pkg.t
+t/cust_bill_pkg_detail.t
+t/cust_credit.t
+t/cust_credit_bill.t
+t/cust_credit_refund.t
+t/cust_main.t
+t/cust_main_county.t
+t/cust_main_invoice.t
+t/cust_pay.t
+t/cust_pay_batch.t
+t/cust_pkg.t
+t/cust_refund.t
+t/cust_svc.t
+t/cust_tax_exempt.t
+t/domain_record.t
+t/nas.t
+t/part_bill_event.t
+t/export_svc.t
+t/part_export.t
+t/part_export_option.t
+t/part_export-bind.t
+t/part_export-bind_slave.t
+t/part_export-bsdshell.t
+t/part_export-cp.t
+t/part_export-cyrus.t
+t/part_export-domain_shellcommands.t
+t/part_export-forward_shellcommands.t
+t/part_export-http.t
+t/part_export-infostreet.t
+t/part_export-ldap.t
+t/part_export-null.t
+t/part_export-shellcommands.t
+t/part_export-shellcommands_withdomain.t
+t/part_export-sqlmail.t
+t/part_export-sqlradius.t
+t/part_export-sysvshell.t
+t/part_export-textradius.t
+t/part_export-vpopmail.t
+t/part_export-www_shellcommands.t
+t/part_pkg.t
+t/part_pop_local.t
+t/part_referral.t
+t/part_svc.t
+t/part_svc_column.t
+t/pkg_svc.t
+t/port.t
+t/prepay_credit.t
+t/radius_usergroup.t
+t/session.t
+t/svc_acct.t
+t/svc_acct_pop.t
+t/svc_Common.t
+t/svc_domain.t
+t/svc_forward.t
+t/svc_www.t
+t/type_pkgs.t
+t/queue.t
+t/queue_arg.t
+t/queue_depend.t
+t/msgcat.t
+t/raddb.t
diff --git a/FS/MANIFEST.SKIP b/FS/MANIFEST.SKIP
new file mode 100644 (file)
index 0000000..ae335e7
--- /dev/null
@@ -0,0 +1 @@
+CVS/
diff --git a/FS/Makefile.PL b/FS/Makefile.PL
new file mode 100644 (file)
index 0000000..1647f8e
--- /dev/null
@@ -0,0 +1,10 @@
+use ExtUtils::MakeMaker;
+# See lib/ExtUtils/MakeMaker.pm for details of how to influence
+# the contents of the Makefile that is written.
+WriteMakefile(
+    'NAME'        => 'FS',
+    'VERSION_FROM' => 'FS.pm', # finds $VERSION
+    'EXE_FILES'    => [ glob 'bin/*' ],
+    'INSTALLSCRIPT'  => '/usr/local/bin',
+    'INSTALLSITEBIN' => '/usr/local/bin',
+);
diff --git a/FS/README b/FS/README
new file mode 100644 (file)
index 0000000..d4c35ac
--- /dev/null
+++ b/FS/README
@@ -0,0 +1,6 @@
+This is the Perl module section of Freeside.
+
+perl Makefile.PL
+make
+make test
+make install
diff --git a/FS/bin/freeside-addoutsource b/FS/bin/freeside-addoutsource
new file mode 100644 (file)
index 0000000..5cec17f
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+domain=$1
+
+createdb $domain && \
+\
+mkdir /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \
+\
+chown freeside /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \
+\
+cp /home/ivan/freeside/conf/[a-z]* /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \
+\
+touch /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \
+\
+chown freeside /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \
+\
+chmod 600 /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \
+\
+echo -e "DBI:Pg:host=localhost;dbname=$domain\nfreeside\n" >/usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain/secrets && \
+\
+mkdir /usr/local/etc/freeside/counters.DBI:Pg:host=localhost\;dbname=$domain && \
+mkdir /usr/local/etc/freeside/cache.DBI:Pg:host=localhost\;dbname=$domain && \
+mkdir /usr/local/etc/freeside/export.DBI:Pg:host=localhost\;dbname=$domain
+
diff --git a/FS/bin/freeside-addoutsourceuser b/FS/bin/freeside-addoutsourceuser
new file mode 100644 (file)
index 0000000..180cd93
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+username=$1
+domain=$2
+password=$3
+
+freeside-adduser -h /usr/local/etc/freeside/htpasswd \
+                 -s conf.DBI:Pg:host=localhost\;dbname=$domain/secrets \
+                 -b \
+                 $username $password 2>/dev/null
+
+[ -e /usr/local/etc/freeside/dbdef.DBI:Pg:host=localhost\;dbname=$domain ] \
+ || ( freeside-setup $username 2>/dev/null; \
+      /home/ivan/freeside/bin/populate-msgcat $username )
+
diff --git a/FS/bin/freeside-adduser b/FS/bin/freeside-adduser
new file mode 100644 (file)
index 0000000..c3ee05b
--- /dev/null
@@ -0,0 +1,63 @@
+#!/usr/bin/perl -w
+#
+# $Id: freeside-adduser,v 1.8 2002-09-27 05:36:29 ivan Exp $
+
+use strict;
+use vars qw($opt_h $opt_b $opt_c $opt_s);
+use Fcntl qw(:flock);
+use Getopt::Std;
+
+my $FREESIDE_CONF = "/usr/local/etc/freeside";
+
+getopts("bch:s:");
+die &usage if $opt_c && ! $opt_h;
+my $user = shift or die &usage;
+
+if ( $opt_h ) {
+  my @args = ( 'htpasswd' );
+  push @args, '-b' if $opt_b;
+  push @args, '-c' if $opt_c;
+  push @args, $opt_h, $user;
+  push @args, shift if $opt_b;
+  system(@args) == 0 or die "htpasswd failed: $?";
+}
+
+my $secretfile = $opt_s || 'secrets';
+
+open(MAPSECRETS,">>$FREESIDE_CONF/mapsecrets")
+  and flock(MAPSECRETS,LOCK_EX)
+    or die "can't open $FREESIDE_CONF/mapsecrets: $!";
+print MAPSECRETS "$user $secretfile\n";
+close MAPSECRETS or die "can't close $FREESIDE_CONF/mapsecrets: $!";
+
+sub usage {
+  die "Usage:\n\n  freeside-adduser [ -h htpasswd_file [ -c ] [ -b ] ] [ -s secretfile ] username"
+}
+
+=head1 NAME
+
+freeside-adduser - Command line interface to add (freeside) users.
+
+=head1 SYNOPSIS
+
+  freeside-adduser [ -h htpasswd_file [ -c ] ] [ -s secretfile ] username
+
+=head1 DESCRIPTION
+
+Adds a user to the Freeside billing system.  This is for adding users (internal
+sales/tech folks) to the web interface, not for adding customer accounts.
+
+  -h: Also call htpasswd for this user with the given filename
+
+  -c: Passed to htpasswd(1)
+
+  -s: Specify an alternate secret file
+
+  -b: same as htpasswd(1), probably insecure, not recommended
+
+=head1 SEE ALSO
+
+L<htpasswd>(1), base Freeside documentation
+
+=cut
+
diff --git a/FS/bin/freeside-apply-credits b/FS/bin/freeside-apply-credits
new file mode 100755 (executable)
index 0000000..ea6a7bd
--- /dev/null
@@ -0,0 +1,21 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use vars qw( $user $cust_main @customers );
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_main;
+
+$user = shift or die &usage;
+&adminsuidsetup( $user );
+
+my @customers = qsearch('cust_main', {} );
+die "No customers" unless (scalar(@customers) > 0);
+
+foreach $cust_main (@customers) {
+  print "Applying credits for customer #". $cust_main->custnum;
+  $cust_main->apply_credits;
+}
+
+
+
diff --git a/FS/bin/freeside-bill b/FS/bin/freeside-bill
new file mode 100755 (executable)
index 0000000..49ad4a7
--- /dev/null
@@ -0,0 +1,128 @@
+#!/usr/bin/perl -w
+# don't take any world-facing input
+#!/usr/bin/perl -Tw
+
+use strict;
+use Fcntl qw(:flock);
+use Date::Parse;
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::cust_main;
+
+&untaint_argv; #what it sounds like  (eww)
+use vars qw($opt_a $opt_c $opt_d $opt_p);
+getopts("acd:p");
+my $user = shift or die &usage;
+
+adminsuidsetup $user;
+
+my %bill_only = map { $_ => 1 } (
+  @ARGV ? @ARGV : ( map $_->custnum, qsearch('cust_main', {} ) )
+);
+
+#we're at now now (and later).
+my($time)= $opt_d ? str2time($opt_d) : $^T;
+
+# find packages w/ bill < time && cancel != '', and create corresponding
+# customer objects
+
+my($cust_main,%saw);
+foreach $cust_main (
+  map {
+    unless ( exists $saw{ $_->custnum } && defined $saw{ $_->custnum} ) {
+      $saw{ $_->custnum } = 0; # to avoid 'use of uninitialized value' errors
+    }
+    if (
+      ( $opt_a || ( ( $_->getfield('bill') || 0 ) <= $time ) )
+      && $bill_only{ $_->custnum }
+      && !$saw{ $_->custnum }++
+    ) {
+      qsearchs('cust_main',{'custnum'=> $_->custnum } );
+    } else {
+      ();
+    }
+  } ( qsearch('cust_pkg', { 'cancel' => '' }),
+      qsearch('cust_pkg', { 'cancel' => 0  }),
+    )
+) {
+
+  # and bill them
+
+  print "Billing customer #" . $cust_main->getfield('custnum') . "\n";
+
+  my($error);
+
+  $error=$cust_main->bill('time'=>$time);
+  warn "Error billing,  customer #" . $cust_main->getfield('custnum') . 
+    ":" . $error if $error;
+
+  if ($opt_p) {
+    $cust_main->apply_payments;
+    $cust_main->apply_credits;
+  }
+
+  if ($opt_c) {
+    $error=$cust_main->collect( 'invoice_time' => $time);
+    warn "Error collecting from customer #" . $cust_main->custnum.  ":$error"
+      if $error;
+
+    #sleep 1;
+  }
+
+}
+
+# subroutines
+
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    # Date::Parse
+    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-bill [ -c [ -p ] ] [ -d 'date' ] user [ custnum custnum ... ]\n";
+}
+
+=head1 NAME
+
+freeside-bill - Command line (crontab, script) interface to customer billing.
+
+=head1 SYNOPSIS
+
+  freeside-bill [ -c [ -p ] [ -a ] ] [ -d 'date' ] user [ custnum custnum ... ]
+
+=head1 DESCRIPTION
+
+This script is deprecated in 1.4.0.  You should use freeside-daily instead.
+
+Bills customers.  Searches for customers who are due for billing and calls
+the bill and collect methods of a cust_main object.  See L<FS::cust_main>.
+
+  -c: Turn on collecting (you probably want this).
+
+  -p: Apply unapplied payments and credits before collecting (you probably want
+      this too)
+
+  -a: Call collect even if there isn't a new invoice (probably a bad idea for
+      daily use)
+
+  -d: Pretend it's 'date'.  Date is in any format Date::Parse is happy with,
+      but be careful.
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+custnum: if one or more customer numbers are specified, only bills those
+customers.  Otherwise, bills all customers.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<freeside-daily>, L<FS::cust_main>, config.html from the base documentation
+
+=cut
+
diff --git a/FS/bin/freeside-cc-receipts-report b/FS/bin/freeside-cc-receipts-report
new file mode 100755 (executable)
index 0000000..136851a
--- /dev/null
@@ -0,0 +1,270 @@
+#!/usr/bin/perl -Tw
+
+
+use strict;
+use Date::Parse;
+use Time::Local;
+use Getopt::Std;
+use Text::Template;
+use Net::SMTP;
+use Mail::Header;
+use Mail::Internet;
+use FS::Conf;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::cust_pay;
+use FS::cust_pay_batch;
+
+
+&untaint_argv; #what it sounds like  (eww)
+use vars qw($opt_v $opt_p $opt_m $opt_e $opt_t $opt_s $opt_f $report_lines $report_template @buf $header);
+getopts("vpmef:s:");   #switches
+
+#we're at now now (and later).
+my($_finishdate)= $opt_f ? str2time($main::opt_f) : $^T;
+my($_startdate)= $opt_s ? str2time($main::opt_s) : $^T;
+
+# Get the current month
+my ($ssec,$smin,$shour,$smday,$smon,$syear) =
+       (localtime($_startdate) )[0,1,2,3,4,5]; 
+$smon++;
+$syear += 1900;
+
+# Get the current month
+my ($fsec,$fmin,$fhour,$fmday,$fmon,$fyear) =
+       (localtime($_finishdate) )[0,1,2,3,4,5]; 
+$fmon++;
+$fyear += 1900;
+
+# Login to the database
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+# Get the needed configuration files
+my $conf = new FS::Conf;
+my $lpr = $conf->config('lpr');
+my $email = $conf->config('email');
+my $smtpmachine = $conf->config('smtpmachine');
+my $mail_sender = $conf->exists('invoice_from') ? $conf->config('invoice_from') :
+  'postmaster';
+my @report_template = $conf->config('report_template')
+  or die "cannot load config file report_template";
+$report_lines = 0;
+foreach ( grep /report_lines\(\d+\)/, @report_template ) { #kludgy :/
+  /report_lines\((\d+)\)/;
+  $report_lines += $1;
+}
+die "no report_lines() functions in template?" unless $report_lines;
+$report_template = new Text::Template (
+  TYPE   => 'ARRAY',
+  SOURCE => [ map "$_\n", @report_template ],
+) or die "can't create new Text::Template object: $Text::Template::ERROR";
+
+
+my(@cust_pays)=qsearch('cust_pay',{});
+if (scalar(@cust_pays) == 0)
+{
+       exit 1;
+}
+
+# Open print and email pipes
+# $lpr and opt_p for printing
+# $email and opt_m for email
+
+if ($lpr && $main::opt_p)
+{
+        open(LPR, "|$lpr");
+}
+
+if ($email && $main::opt_m)
+{
+  $ENV{MAILADDRESS} = $mail_sender;
+  $header = new Mail::Header ( [
+    "From: Account Processor",
+    "To: $email",
+    "Sender: $mail_sender",
+    "Reply-To: $mail_sender",
+    "Subject: Credit Card Receipts",
+  ] );
+}
+
+my $uninvoiced = 0;
+my $total = 0;
+my $taxed = 0;
+my $untaxed = 0;
+my $total_tax = 0;
+
+# Now I can start looping
+foreach my $cust_pay (@cust_pays)
+{
+       my $_date = $cust_pay->getfield('_date');
+       my $invnum = $cust_pay->getfield('invnum');
+       my $paid = $cust_pay->getfield('paid');
+       my $payby = $cust_pay->getfield('payby');
+       
+
+       if ($_date >= $_startdate && $_date <= $_finishdate && $payby =~ 'CARD') {
+               $total += $paid;
+
+               $uninvoiced += $cust_pay->unapplied; 
+               my @cust_bill_pays = $cust_pay->cust_bill_pay;
+                foreach my $cust_bill_pay (@cust_bill_pays) {
+                       my $invoice_amt =0;
+                       my $invoice_tax =0;
+                       my(@cust_bill_pkgs)= $cust_bill_pay->cust_bill->cust_bill_pkg;
+                       foreach my $cust_bill_pkg (@cust_bill_pkgs) {
+
+                               my $recur = $cust_bill_pkg->getfield('recur');
+                               my $setup = $cust_bill_pkg->getfield('setup');
+                               my $pkgnum = $cust_bill_pkg->getfield('pkgnum');
+                       
+                               if ($pkgnum == 0) {
+                                       $invoice_tax += $recur;
+                                       $invoice_tax += $setup;
+                               } else {
+                                       $invoice_amt += $recur;
+                                       $invoice_amt += $setup;
+                               }
+
+                       }
+
+                       if ($invoice_tax > 0) {
+                               if ($invoice_amt != $paid) {
+                                       # attempt to prorate partially paid invoices
+                                       $total_tax += $paid / ($invoice_amt + $invoice_tax) * $invoice_tax;
+                                       $taxed += $paid / ($invoice_amt + $invoice_tax) * $invoice_amt;
+                               } else {
+                                       $total_tax += $invoice_tax;
+                                       $taxed += $invoice_amt;
+                               }
+                       } else {
+                               $untaxed += $paid;
+                       }
+
+               }
+
+       }
+
+}
+
+push @buf, sprintf(qq{\n%25s%14.2f\n}, "Uninvoiced", $uninvoiced);
+push @buf, sprintf(qq{%25s%14.2f\n}, "Untaxed", $untaxed);
+push @buf, sprintf(qq{%25s%14.2f\n}, "Taxed", $taxed);
+push @buf, sprintf(qq{%25s%14.2f\n}, "Tax", $total_tax);
+push @buf, sprintf(qq{\n%39s\n%39.2f\n}, "=========", $total);
+
+sub FS::cc_receipts_report::_template::report_lines {
+  my $lines = shift;
+  map {
+    scalar(@buf) ? shift @buf : '' ;
+  }
+  ( 1 .. $lines );
+}
+
+$FS::cc_receipts_report::_template::title = qq~CREDIT CARD RECEIPTS for period $smon/$smday/$syear through $fmon/$fmday/$fyear~;
+$FS::cc_receipts_report::_template::title = $opt_t if $opt_t;
+$FS::cc_receipts_report::_template::page = 1;
+$FS::cc_receipts_report::_template::date = $^T;
+$FS::cc_receipts_report::_template::date = $^T;
+$FS::cc_receipts_report::_template::fdate = $_finishdate;
+$FS::cc_receipts_report::_template::fdate = $_finishdate;
+$FS::cc_receipts_report::_template::sdate = $_startdate;
+$FS::cc_receipts_report::_template::sdate = $_startdate;
+$FS::cc_receipts_report::_template::total_pages = 
+  int( scalar(@buf) / $report_lines);
+$FS::cc_receipts_report::_template::total_pages++ if scalar(@buf) % $report_lines;
+
+my @report;
+while (@buf) {
+  push @report, split("\n", 
+    $report_template->fill_in( PACKAGE => 'FS::cc_receipts_report::_template' )
+  );
+  $FS::cc_receipts_report::_template::page++;
+}
+
+if ($opt_v) {
+  print map "$_\n", @report;
+}
+if($lpr && $opt_p)
+{
+  print LPR map "$_\n", @report;
+  print LPR "\f" if $opt_e;
+  close LPR || die "Could not close printer: $lpr\n";
+}
+if($email && $opt_m)
+{
+  my $message = new Mail::Internet (
+    'Header' => $header,
+    'Body' => [ (@report) ],
+  );
+  $!=0;
+  $message->smtpsend( Host => "$smtpmachine" )
+    or die "can't send report to $email via $smtpmachine: $!";
+}
+
+
+# subroutines
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    $ARGV[$_] =~ /^([\w\-\/ :\.]*)$/ || die "Illegal argument \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-cc-receipts-report [-v] [-p] [-e] user\n";
+}
+
+=head1 NAME
+
+freeside-cc-receipts-report - Prints or emails total credit card receipts in a given period.
+
+=head1 SYNOPSIS
+
+  freeside-cc-receipts-report [-v] [-p] [-m] [-e] [-t "title"] [-s date] [-f date] user
+
+=head1 DESCRIPTION
+
+Prints or emails sales taxes invoiced in a given period.
+
+-v: Verbose - Prints records to STDOUT.
+
+-p: Print to printer lpr as found in the conf directory.
+
+-m: Email output to user found in the Conf email file.
+
+-e: Print a final form feed to the printer.
+
+-t: supply a title for the top of each page.
+
+-s: starting date for inclusion
+
+-f: final date for inclusion
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+=head1 VERSION
+
+$Id: freeside-cc-receipts-report,v 1.5 2002-09-09 22:57:34 ivan Exp $
+
+=head1 BUGS
+
+Yes..... Use at your own risk. No guarantees or warrantees of any
+kind apply to this program. Parts of this program are hacked from
+other GNU licensed software created mainly by Ivan Kohler.
+
+This is released under the GNU Public License. See www.gnu.org
+for more information regarding this license.
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, config.html from the base documentation
+
+=head1 AUTHOR
+
+Jeff Finucane <jeff@cmh.net>
+
+based on print-batch by Joel Griffiths <griff@aver-computer.com>
+
+=cut
+
diff --git a/FS/bin/freeside-count-active-customers b/FS/bin/freeside-count-active-customers
new file mode 100755 (executable)
index 0000000..759085a
--- /dev/null
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+domain=$1
+
+echo "\t
+select count(*) from cust_main where 
+          0 < ( SELECT COUNT(*) FROM cust_pkg
+                       WHERE cust_pkg.custnum = cust_main.custnum
+                         AND ( cust_pkg.cancel IS NULL
+                               OR cust_pkg.cancel = 0
+                             )
+                   )
+            OR 0 = ( SELECT COUNT(*) FROM cust_pkg
+                       WHERE cust_pkg.custnum = cust_main.custnum
+                   );
+" | psql -U freeside -q $domain | head -1
+
diff --git a/FS/bin/freeside-credit-report b/FS/bin/freeside-credit-report
new file mode 100755 (executable)
index 0000000..410dabe
--- /dev/null
@@ -0,0 +1,224 @@
+#!/usr/bin/perl -Tw
+
+
+use strict;
+use Date::Parse;
+use Time::Local;
+use Getopt::Std;
+use Text::Template;
+use Net::SMTP;
+use Mail::Header;
+use Mail::Internet;
+use FS::Conf;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_credit;
+
+
+&untaint_argv; #what it sounds like  (eww)
+use vars qw($opt_v $opt_p $opt_m $opt_e $opt_t $opt_s $opt_f $report_lines $report_template @buf $header);
+getopts("vpmef:s:");   #switches
+
+#we're at now now (and later).
+my($_finishdate)= $opt_f ? str2time($main::opt_f) : $^T;
+my($_startdate)= $opt_s ? str2time($main::opt_s) : $^T;
+
+# Get the current month
+my ($ssec,$smin,$shour,$smday,$smon,$syear) =
+       (localtime($_startdate) )[0,1,2,3,4,5]; 
+$smon++;
+$syear += 1900;
+
+# Get the current month
+my ($fsec,$fmin,$fhour,$fmday,$fmon,$fyear) =
+       (localtime($_finishdate) )[0,1,2,3,4,5]; 
+$fmon++;
+$fyear += 1900;
+
+# Login to the database
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+# Get the needed configuration files
+my $conf = new FS::Conf;
+my $lpr = $conf->config('lpr');
+my $email = $conf->config('email');
+my $smtpmachine = $conf->config('smtpmachine');
+my $mail_sender = $conf->exists('invoice_from') ? $conf->config('invoice_from') :
+  'postmaster';
+my @report_template = $conf->config('report_template')
+  or die "cannot load config file report_template";
+$report_lines = 0;
+foreach ( grep /report_lines\(\d+\)/, @report_template ) { #kludgy :/
+  /report_lines\((\d+)\)/;
+  $report_lines += $1;
+}
+die "no report_lines() functions in template?" unless $report_lines;
+$report_template = new Text::Template (
+  TYPE   => 'ARRAY',
+  SOURCE => [ map "$_\n", @report_template ],
+) or die "can't create new Text::Template object: $Text::Template::ERROR";
+
+
+my(@cust_credits)=qsearch('cust_credit',{});
+if (scalar(@cust_credits) == 0)
+{
+       exit 1;
+}
+
+# Open print and email pipes
+# $lpr and opt_p for printing
+# $email and opt_m for email
+
+if ($lpr && $main::opt_p)
+{
+        open(LPR, "|$lpr");
+}
+
+if ($email && $main::opt_m)
+{
+  $ENV{MAILADDRESS} = $mail_sender;
+  $header = new Mail::Header ( [
+    "From: Account Processor",
+    "To: $email",
+    "Sender: $mail_sender",
+    "Reply-To: $mail_sender",
+    "Subject: In House Credits",
+  ] );
+}
+
+my $uninvoiced = 0;
+my $total = 0;
+my $taxed = 0;
+my $untaxed = 0;
+my $total_tax = 0;
+
+# Now I can start looping
+foreach my $cust_credit (@cust_credits)
+{
+       my $_date = $cust_credit->getfield('_date');
+       my $amount = $cust_credit->getfield('amount');
+
+       if ($_date >= $_startdate && $_date <= $_finishdate) {
+               $total += $amount;
+       }
+}
+
+push @buf, sprintf(qq{\n%25s%14.2f\n}, "Credits Offered", $total);
+push @buf, sprintf(qq{\n%39s\n%39.2f\n}, "=========", $total);
+
+sub FS::credit_report::_template::report_lines {
+  my $lines = shift;
+  map {
+    scalar(@buf) ? shift @buf : '' ;
+  }
+  ( 1 .. $lines );
+}
+
+$FS::credit_report::_template::title = qq~IN HOUSE CREDITS for $smon/$smday/$syear through $fmon/$fmday/$fyear~;
+$FS::credit_report::_template::title = $opt_t if $opt_t;
+$FS::credit_report::_template::page = 1;
+$FS::credit_report::_template::date = $^T;
+$FS::credit_report::_template::date = $^T;
+$FS::credit_report::_template::fdate = $_finishdate;
+$FS::credit_report::_template::fdate = $_finishdate;
+$FS::credit_report::_template::sdate = $_startdate;
+$FS::credit_report::_template::sdate = $_startdate;
+$FS::credit_report::_template::total_pages = 
+  int( scalar(@buf) / $report_lines);
+$FS::credit_report::_template::total_pages++ if scalar(@buf) % $report_lines;
+
+my @report;
+while (@buf) {
+  push @report, split("\n", 
+    $report_template->fill_in( PACKAGE => 'FS::credit_report::_template' )
+  );
+  $FS::credit_report::_template::page++;
+}
+
+if ($opt_v) {
+  print map "$_\n", @report;
+}
+if($lpr && $opt_p)
+{
+  print LPR map "$_\n", @report;
+  print LPR "\f" if $opt_e;
+  close LPR || die "Could not close printer: $lpr\n";
+}
+if($email && $opt_m)
+{
+  my $message = new Mail::Internet (
+    'Header' => $header,
+    'Body' => [ (@report) ],
+  );
+  $!=0;
+  $message->smtpsend( Host => "$smtpmachine" )
+    or die "can't send report to $email via $smtpmachine: $!";
+}
+
+
+# subroutines
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    $ARGV[$_] =~ /^([\w\-\/ :\.]*)$/ || die "Illegal argument \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-credit-report [-v] [-p] [-e] user\n";
+}
+
+=head1 NAME
+
+freeside-credit-report - Prints or emails total credit memos in a given period.
+
+=head1 SYNOPSIS
+
+  freeside-credit-report [-v] [-p] [-m] [-e] [-t "title"] [-s date] [-f date] user
+
+=head1 DESCRIPTION
+
+Prints or emails total credit memos in a given period.
+
+-v: Verbose - Prints records to STDOUT.
+
+-p: Print to printer lpr as found in the conf directory.
+
+-m: Email output to user found in the Conf email file.
+
+-e: Print a final form feed to the printer.
+
+-t: supply a title for the top of each page.
+
+-s: starting date for inclusion
+
+-f: final date for inclusion
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+=head1 VERSION
+
+$Id: freeside-credit-report,v 1.5 2002-09-09 22:57:34 ivan Exp $
+
+=head1 BUGS
+
+Yes..... Use at your own risk. No guarantees or warrantees of any
+kind apply to this program. Parts of this program are hacked from
+other GNU licensed software created mainly by Ivan Kohler.
+
+This is released under the GNU Public License. See www.gnu.org
+for more information regarding this license.
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, config.html from the base documentation
+
+=head1 AUTHOR
+
+Jeff Finucane <jeff@cmh.net>
+
+based on print-batch by Joel Griffiths <griff@aver-computer.com>
+
+=cut
+
diff --git a/FS/bin/freeside-daily b/FS/bin/freeside-daily
new file mode 100755 (executable)
index 0000000..63e621b
--- /dev/null
@@ -0,0 +1,132 @@
+#!/usr/bin/perl -w
+
+use strict;
+use Fcntl qw(:flock);
+use Date::Parse;
+use Getopt::Std;
+use FS::UID qw(adminsuidsetup driver_name dbh datasrc);
+use FS::Record qw(qsearch qsearchs);
+use FS::Conf;
+use FS::cust_main;
+
+&untaint_argv; #what it sounds like  (eww)
+use vars qw($opt_d $opt_v $opt_p);
+getopts("p:d:v");
+my $user = shift or die &usage;
+
+adminsuidsetup $user;
+
+$FS::cust_main::Debug = 1 if $opt_v;
+
+my %search;
+$search{'payby'} = $opt_p if $opt_p;
+
+my @cust_main = @ARGV
+  ? map { qsearchs('cust_main', { custnum => $_, %search } ) } @ARGV
+  : qsearch('cust_main', \%search )
+;
+
+#we're at now now (and later).
+my($time)= $opt_d ? str2time($opt_d) : $^T;
+
+my($cust_main,%saw);
+foreach $cust_main ( @cust_main ) {
+
+  # $^T not $time because -d is for pre-printing invoices
+  foreach my $cust_pkg (
+    grep { $_->expire && $_->expire <= $^T } $cust_main->ncancelled_pkgs
+  ) {
+    my $error = $cust_pkg->cancel;
+    warn "Error cancelling expired pkg ". $cust_pkg->pkgnum. " for custnum ".
+         $cust_main->custnum. ": $error"
+      if $error;
+  }
+
+  my $error = $cust_main->bill( 'time' => $time );
+  warn "Error billing, custnum ". $cust_main->custnum. ": $error" if $error;
+
+  $cust_main->apply_payments;
+  $cust_main->apply_credits;
+
+  $error = $cust_main->collect( 'invoice_time' => $time );
+  warn "Error collecting, custnum". $cust_main->custnum. ": $error" if $error;
+
+}
+
+if ( driver_name eq 'Pg' ) {
+  dbh->{AutoCommit} = 1; #so we can vacuum
+  foreach my $statement ( 'vacuum', 'vacuum analyze' ) {
+    my $sth = dbh->prepare($statement) or die dbh->errstr;
+    $sth->execute or die $sth->errstr;
+  }
+}
+
+#local hack
+my $conf = new FS::Conf;
+my $dest = $conf->config('dump-scpdest');
+if ( $dest ) {
+  datasrc =~ /dbname=([\w\.]+)$/ or die "unparsable datasrc ". datasrc;
+  my $database = $1;
+  eval "use Net::SCP qw(scp);";
+  if ( driver_name eq 'Pg' ) {
+    system("pg_dump $database >/var/tmp/$database.sql")
+  } else {
+    die "database dumps not yet supported for ". driver_name;
+  }
+  scp("/var/tmp/$database.sql", $dest);
+  unlink "/var/tmp/$database.sql" or die $!;
+}
+
+# subroutines
+
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    # Date::Parse
+    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-daily [ -d 'date' ] user [ custnum custnum ... ]\n";
+}
+
+=head1 NAME
+
+freeside-daily - Run daily billing and invoice collection events.
+
+=head1 SYNOPSIS
+
+  freeside-daily [ -d 'date' ] [ -p 'payby' ] [ -v ] user [ custnum custnum ... ]
+
+=head1 DESCRIPTION
+
+Bills customers and runs invoice collection events.  Should be run from
+crontab daily.
+
+This script replaces freeside-bill from 1.3.1.
+
+Bills customers.  Searches for customers who are due for billing and calls
+the bill and collect methods of a cust_main object.  See L<FS::cust_main>.
+
+  -d: Pretend it's 'date'.  Date is in any format Date::Parse is happy with,
+      but be careful.
+
+  -p: Only process customers with the specified payby (I<CARD>, I<DCRD>, I<CHEK>, I<DCHK>, I<BILL>, I<COMP>, I<LECB>)
+
+  -v: enable debugging
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+custnum: if one or more customer numbers are specified, only bills those
+customers.  Otherwise, bills all customers.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, config.html from the base documentation
+
+=cut
+
diff --git a/FS/bin/freeside-deloutsource b/FS/bin/freeside-deloutsource
new file mode 100644 (file)
index 0000000..5618535
--- /dev/null
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+domain=$1
+
+dropdb $domain && \
+rm -rf /usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=$domain && \
+rm -rf /usr/local/etc/freeside/counters.DBI:Pg:host=localhost\;dbname=$domain && \
+rm -rf /usr/local/etc/freeside/cache.DBI:Pg:host=localhost\;dbname=$domain && \
+rm -rf /usr/local/etc/freeside/export.DBI:Pg:host=localhost\;dbname=$domain && \
+rm /usr/local/etc/freeside/dbdef.DBI:Pg:host=localhost\;dbname=$domain
+
diff --git a/FS/bin/freeside-deloutsourceuser b/FS/bin/freeside-deloutsourceuser
new file mode 100644 (file)
index 0000000..96871e5
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+username=$1
+
+freeside-deluser -h /usr/local/etc/freeside/htpasswd $username 2>/dev/null
+
diff --git a/FS/bin/freeside-deluser b/FS/bin/freeside-deluser
new file mode 100644 (file)
index 0000000..57d6ce1
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw($opt_h);
+use Fcntl qw(:flock);
+use Getopt::Std;
+
+my $FREESIDE_CONF = "/usr/local/etc/freeside";
+
+getopts("h:");
+my $user = shift or die &usage;
+
+if ( $opt_h ) {
+  open(HTPASSWD,"<$opt_h")
+    and flock(HTPASSWD,LOCK_EX)
+      or die "can't open $opt_h: $!";
+  open(HTPASSWD_TMP,">$opt_h.tmp") or die "can't open $opt_h.tmp: $!";
+  while (<HTPASSWD>) {
+    print HTPASSWD_TMP $_ unless /^$user:/;
+  }
+  close HTPASSWD_TMP;
+  rename "$opt_h.tmp", "$opt_h" or die $!;
+  flock(HTPASSWD,LOCK_UN);
+  close HTPASSWD;
+}
+
+open(MAPSECRETS,"<$FREESIDE_CONF/mapsecrets")
+  and flock(MAPSECRETS,LOCK_EX)
+    or die "can't open $FREESIDE_CONF/mapsecrets: $!";
+open(MAPSECRETS_TMP,">>$FREESIDE_CONF/mapsecrets.tmp")
+  or die "can't open $FREESIDE_CONF/mapsecrets.tmp: $!";
+while (<MAPSECRETS>) {
+  print MAPSECRETS_TMP $_ unless /^$user\s/;
+}
+close MAPSECRETS_TMP;
+rename "$FREESIDE_CONF/mapsecrets.tmp", "$FREESIDE_CONF/mapsecrets" or die $!;
+flock(MAPSECRETS,LOCK_UN);
+close MAPSECRETS;
+
+sub usage {
+  die "Usage:\n\n  freeside-deluser [ -h htpasswd_file ] username"
+}
+
+=head1 NAME
+
+freeside-deluser - Command line interface to add (freeside) users.
+
+=head1 SYNOPSIS
+
+  freeside-deluser [ -h htpasswd_file ] username
+
+=head1 DESCRIPTION
+
+Adds a user to the Freeside billing system.  This is for adding users (internal
+sales/tech folks) to the web interface, not for adding customer accounts.
+
+  -h: Also delete from the given htpasswd filename
+
+=head1 SEE ALSO
+
+L<freeside-adduser>, L<htpasswd>(1), base Freeside documentation
+
+=cut
+
diff --git a/FS/bin/freeside-email b/FS/bin/freeside-email
new file mode 100755 (executable)
index 0000000..400dc2a
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw(qsearch);
+use FS::svc_acct;
+
+&untaint_argv; #what it sounds like  (eww)
+my $user = shift or die &usage;
+
+adminsuidsetup $user;
+
+my $conf = new FS::Conf;
+
+my @svc_acct = qsearch('svc_acct', {});
+my @emails = map $_->email, @svc_acct;
+
+print join("\n", @emails), "\n";
+
+# subroutines
+
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    #$ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    # Date::Parse
+    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-email user\n";
+}
+
+=head1 NAME
+
+freeside-email - Prints email addresses of all users on STDOUT
+
+=head1 SYNOPSIS
+
+  freeside-email user
+
+=head1 DESCRIPTION
+
+Prints the email addresses of all customers on STDOUT, separated by newlines.
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+=head1 VERSION
+
+$Id: freeside-email,v 1.2 2002-09-18 22:50:44 ivan Exp $
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+=cut
+
diff --git a/FS/bin/freeside-expiration-alerter b/FS/bin/freeside-expiration-alerter
new file mode 100755 (executable)
index 0000000..691fd3a
--- /dev/null
@@ -0,0 +1,226 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use Date::Format;
+use Time::Local;
+use Text::Template;
+use Getopt::Std;
+use Net::SMTP;
+use Mail::Header;
+use Mail::Internet;
+use FS::Conf;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_main;
+
+use vars qw($smtpmachine @body);
+
+#hush, perl!
+$FS::alerter::_template::first = "";
+$FS::alerter::_template::last = "";
+$FS::alerter::_template::company = "";
+$FS::alerter::_template::payby = "";
+$FS::alerter::_template::expdate = "";
+
+# Set the mail program  and other variables
+my $mail_sender = "billing\@mydomain.tld";  # or invoice_from if available
+my $failure_recipient = "postmaster";       # or invoice_from if available
+my $warning_time = 30 * 24 * 60 * 60;
+my $urgent_time = 15 * 24 * 60 * 60;
+my $panic_time = 5 * 24 * 60 * 60;
+my $window_time = 24 * 60 * 60;
+
+&untaint_argv; #what it sounds like  (eww)
+
+#we're at now now (and later).
+my($_date)= $^T;
+
+# Get the current month
+my ($sec,$min,$hour,$mday,$mon,$year) =
+       (localtime($_date) )[0,1,2,3,4,5]; 
+$mon++;
+
+# Login to the database
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+# Get the needed configuration files
+my $conf = new FS::Conf;
+$smtpmachine = $conf->config('smtpmachine');
+$mail_sender = $conf->config('invoice_from')
+  if $conf->exists('invoice_from');
+$failure_recipient = $conf->config('invoice_from')
+  if $conf->exists('invoice_from');
+
+
+my(@customers)=qsearch('cust_main',{});
+if (scalar(@customers) == 0)
+{
+  exit 1;
+}
+
+# Prepare for sending email
+
+$ENV{MAILADDRESS} = $mail_sender;
+my $header = new Mail::Header ( [
+  "From: Account Processor",
+  "To: $failure_recipient",
+  "Sender: $mail_sender",
+  "Reply-To: $mail_sender",
+  "Subject: Unnotified Billing Arrangement Expirations",
+] );
+
+my @alerter_template = $conf->config('alerter_template')
+  or die "cannot load config file alerter_template";
+
+my $alerter = new Text::Template (TYPE => 'ARRAY', SOURCE => [ map "$_\n", @alerter_template ])
+  or die "can't create new Text::Template object:  Text::Template::ERROR";
+$alerter->compile() or die "can't compile template:  Text::Template::ERROR";
+
+# Now I can start looping
+foreach my $customer (@customers)
+{
+  my $paydate = $customer->getfield('paydate');
+  next if $paydate =~ /^\s*$/; #skip empty expiration dates
+
+  my $custnum = $customer->getfield('custnum');
+  my $first = $customer->getfield('first');
+  my $last = $customer->getfield('last');
+  my $company = $customer->getfield('company');
+  my $payby = $customer->getfield('payby');
+  my $payinfo = $customer->getfield('payinfo');
+  my $daytime = $customer->getfield('daytime');
+  my $night = $customer->getfield('night');
+
+  my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
+
+  my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
+
+  #credit cards expire at the end of the month/year of their exp date
+  if ($payby eq 'CARD' || $payby eq 'DCRD') {
+    ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
+    $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
+    $expire_time--;
+  }
+
+  if (($expire_time < $_date + $warning_time &&
+    $expire_time > $_date + $warning_time - $window_time) ||
+      ($expire_time < $_date + $urgent_time &&
+       $expire_time > $_date + $urgent_time - $window_time) ||
+      ($expire_time < $_date + $panic_time &&
+       $expire_time > $_date + $panic_time - $window_time)) {
+
+
+
+    my @packages = $customer->ncancelled_pkgs;
+    if (scalar(@packages) != 0) {
+      my @invoicing_list = $customer->invoicing_list;
+      if ( grep { $_ ne 'POST' } @invoicing_list ) { 
+        my $header = new Mail::Header ( [
+          "From: $mail_sender",
+          "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
+          "Sender: $mail_sender",
+          "Reply-To: $mail_sender",
+          "Date: ". time2str("%a, %d %b %Y %X %z", time),
+          "Subject: Billing Arrangement Expiration",
+        ] );
+        $FS::alerter::_template::first = $first;
+        $FS::alerter::_template::last = $last;
+        $FS::alerter::_template::company = $company;
+        if ($payby eq 'CARD' || $payby eq 'DCRD') {
+          $FS::alerter::_template::payby = "credit card (" .
+            substr($payinfo, 0, 2) . "xxxxxxxxxx" .
+            substr($payinfo, -4) . ")";
+        }elsif ($payby eq 'COMP') {
+          $FS::alerter::_template::payby = "complimentary account";
+        }else{
+          $FS::alerter::_template::payby = "current method";
+        }
+        $FS::alerter::_template::expdate = $expire_time;
+
+        my $message = new Mail::Internet (
+          'Header' => $header,
+          'Body' => [ $alerter->fill_in( PACKAGE => 'FS::alerter::_template' ) ],
+        );
+        $!=0;
+        $message->smtpsend( Host => $smtpmachine )
+          or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
+            or die "Can't send expiration email: $!";
+
+      } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) { 
+        push @body, sprintf(qq{%5d %-32.32s %4s %10s %12s %12s},
+          $custnum,
+          $first . " " . $last . "   " . $company,
+          $payby,
+          $paydate,
+          $daytime,
+          $night);
+      }
+    }
+  }
+}
+
+# Now I need to send EMAIL
+if (scalar(@body)) {
+  my $message = new Mail::Internet (
+    'Header' => $header,
+    'Body' => [ (@body) ],
+  );
+  $!=0;
+  $message->smtpsend( Host => $smtpmachine )
+    or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
+      or die "can't send alerter failure email to $failure_recipient".
+             " via server $smtpmachine with SMTP: $!";
+}
+
+# subroutines
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    $ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal argument \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-expiration-alerter user\n";
+}
+
+=head1 NAME
+
+freeside-expiration-alerter - Emails notifications of credit card expirations.
+
+=head1 SYNOPSIS
+
+  freeside-expiration-alerter user
+
+=head1 DESCRIPTION
+
+Emails customers notice that their credit card or other billing arrangement
+is about to expire.  Usually run as a cron job.
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+=head1 VERSION
+
+$Id: freeside-expiration-alerter,v 1.5 2003-04-21 20:53:57 ivan Exp $
+
+=head1 BUGS
+
+Yes..... Use at your own risk. No guarantees or warrantees of any
+kind apply to this program. Parts of this program are hacked from
+other GNU licensed software created mainly by Ivan Kohler.
+
+This is released under the GNU Public License. See www.gnu.org
+for more information regarding this license.
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, config.html from the base documentation
+
+=head1 AUTHOR
+
+Jeff Finucane <jeff@cmh.net>
+
+=cut
+
+
diff --git a/FS/bin/freeside-queued b/FS/bin/freeside-queued
new file mode 100644 (file)
index 0000000..6ea27c0
--- /dev/null
@@ -0,0 +1,267 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw( $log_file $sigterm $sigint $kids $max_kids %kids );
+use subs qw( _die _logmsg );
+use Fcntl qw(:flock);
+use POSIX qw(:sys_wait_h setsid);
+use Date::Format;
+use IO::File;
+use FS::UID qw(adminsuidsetup forksuidsetup driver_name dbh);
+use FS::Record qw(qsearch qsearchs);
+use FS::queue;
+use FS::queue_depend;
+
+# no autoloading just yet
+use FS::cust_main;
+use FS::svc_acct;
+use Net::SSH 0.07;
+use FS::part_export;
+
+$max_kids = '10'; #guess it should be a config file...
+$kids = 0;
+
+my $user = shift or die &usage;
+
+#my $pid_file = "/var/run/freeside-queued.$user.pid";
+my $pid_file = "/var/run/freeside-queued.pid";
+
+&daemonize1;
+
+#sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; }
+#$SIG{CHLD} =  \&REAPER;
+
+$sigterm = 0;
+$sigint = 0;
+$SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $sigint++; };
+$SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $sigterm++; };
+
+my $freeside_gid = scalar(getgrnam('freeside'))
+  or die "can't setgid to freeside group\n";
+$) = $freeside_gid;
+$( = $freeside_gid;
+#if freebsd can't setuid(), presumably it can't setgid() either.  grr fleabsd
+($(,$)) = ($),$();
+$) = $freeside_gid;
+
+$> = $FS::UID::freeside_uid;
+$< = $FS::UID::freeside_uid;
+#freebsd is sofa king broken, won't setuid()
+($<,$>) = ($>,$<);
+$> = $FS::UID::freeside_uid;
+
+$ENV{HOME} = (getpwuid($>))[7]; #for ssh
+adminsuidsetup $user;
+
+$log_file = "/usr/local/etc/freeside/queuelog.". $FS::UID::datasrc;
+
+&daemonize2;
+
+$SIG{__DIE__} = \&_die;
+$SIG{__WARN__} = \&_logmsg;
+
+warn "freeside-queued starting\n";
+
+my $warnkids=0;
+while (1) {
+
+  &reap_kids;
+  #prevent runaway forking
+  if ( $kids >= $max_kids ) {
+    warn "WARNING: maximum $kids children reached\n" unless $warnkids++;
+    &reap_kids;
+    sleep 1; #waiting for signals is cheap
+    next;
+  }
+  $warnkids=0;
+
+  my $nodepend = driver_name eq 'mysql'
+   ? ''
+   : 'AND 0 = ( SELECT COUNT(*) FROM queue_depend'.
+     ' WHERE queue_depend.jobnum = queue.jobnum ) ';
+
+  #my($job, $ljob);
+  #{
+  #  my $oldAutoCommit = $FS::UID::AutoCommit;
+  #  local $FS::UID::AutoCommit = 0;
+  $FS::UID::AutoCommit = 0;
+  my $dbh = dbh; 
+  
+  my $job = qsearchs(
+    'queue',
+    { 'status' => 'new' },
+    '',
+    driver_name eq 'mysql'
+      ? "$nodepend ORDER BY jobnum LIMIT 1 FOR UPDATE"
+      : "$nodepend ORDER BY jobnum FOR UPDATE LIMIT 1"
+  ) or do {
+    $dbh->commit or die $dbh->errstr; #if $oldAutoCommit;
+    sleep 5; #connecting to db is expensive
+    next;
+  };
+
+  if ( driver_name eq 'mysql'
+       && qsearch('queue_depend', { 'jobnum' => $job->jobnum } ) ) {
+    $dbh->commit or die $dbh->errstr; #if $oldAutoCommit;
+    sleep 5; #would be better if mysql could do everything in query above
+    next;
+  }
+
+  my %hash = $job->hash;
+  $hash{'status'} = 'locked';
+  my $ljob = new FS::queue ( \%hash );
+  my $error = $ljob->replace($job);
+  die $error if $error;
+
+  $dbh->commit or die $dbh->errstr; #if $oldAutoCommit;
+
+  $FS::UID::AutoCommit = 1;
+  #} 
+
+  my @args = $ljob->args;
+
+  defined( my $pid = fork ) or do {
+    warn "WARNING: can't fork: $!\n";
+    my %hash = $job->hash;
+    $hash{'status'} = 'failed';
+    $hash{'statustext'} = "[freeside-queued] can't fork: $!";
+    my $ljob = new FS::queue ( \%hash );
+    my $error = $ljob->replace($job);
+    die $error if $error;
+    next; #don't increment the kid counter
+  };
+
+  if ( $pid ) {
+    $kids++;
+    $kids{$pid} = 1;
+  } else { #kid time
+
+    #get new db handle
+    $FS::UID::dbh->{InactiveDestroy} = 1;
+
+    forksuidsetup($user);
+
+    #auto-use export classes...
+    if ( $ljob->job =~ /(FS::part_export::\w+)::/ ) {
+      my $class = $1;
+      eval "use $class;";
+      if ( $@ ) {
+        warn "job use $class failed";
+        my %hash = $ljob->hash;
+        $hash{'status'} = 'failed';
+        $hash{'statustext'} = $@;
+        my $fjob = new FS::queue( \%hash );
+        my $error = $fjob->replace($ljob);
+        die $error if $error;
+        exit; #end-of-kid
+      };
+    }
+
+    my $eval = "&". $ljob->job. '(@args);';
+    warn "running $eval";
+    eval $eval; #throw away return value?  suppose so
+    if ( $@ ) {
+      warn "job $eval failed";
+      my %hash = $ljob->hash;
+      $hash{'status'} = 'failed';
+      $hash{'statustext'} = $@;
+      my $fjob = new FS::queue( \%hash );
+      my $error = $fjob->replace($ljob);
+      die $error if $error;
+    } else {
+      $ljob->delete;
+    }
+
+    exit;
+    #end-of-kid
+  }
+
+} continue {
+  if ( $sigterm ) {
+    warn "received TERM signal; exiting\n";
+    exit;
+  }
+  if ( $sigint ) {
+    warn "received INT signal; exiting\n";
+    exit;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-queued user\n";
+}
+
+sub _die {
+  my $msg = shift;
+  unlink $pid_file if -e $pid_file;
+  _logmsg($msg);
+}
+
+sub _logmsg {
+  chomp( my $msg = shift );
+  my $log = new IO::File ">>$log_file";
+  flock($log, LOCK_EX);
+  seek($log, 0, 2);
+  print $log "[". time2str("%a %b %e %T %Y",time). "] [$$] $msg\n";
+  flock($log, LOCK_UN);
+  close $log;
+}
+
+sub daemonize1 {
+
+  chdir "/" or die "Can't chdir to /: $!";
+  open STDIN, '/dev/null'   or die "Can't read /dev/null: $!";
+  defined(my $pid = fork) or die "Can't fork: $!";
+  if ( $pid ) {
+    print "freeside-queued started with pid $pid\n"; #logging to $log_file\n";
+    exit unless $pid_file;
+    my $pidfh = new IO::File ">$pid_file" or exit;
+    print $pidfh "$pid\n";
+    exit;
+  }
+  #open STDOUT, '>/dev/null'
+  #                          or die "Can't write to /dev/null: $!";
+  #setsid                  or die "Can't start a new session: $!";
+  #open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
+
+}
+
+sub daemonize2 {
+  open STDOUT, '>/dev/null'
+                            or die "Can't write to /dev/null: $!";
+  setsid                  or die "Can't start a new session: $!";
+  open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
+}
+
+sub reap_kids {
+  foreach my $pid ( keys %kids ) {
+    my $kid = waitpid($pid, WNOHANG);
+    if ( $kid > 0 ) {
+      $kids--;
+      delete $kids{$kid};
+    }
+  }
+}
+
+=head1 NAME
+
+freeside-queued - Job queue daemon
+
+=head1 SYNOPSIS
+
+  freeside-queued user
+
+=head1 DESCRIPTION
+
+Job queue daemon.  Should be running at all times.
+
+user: from the mapsecrets file - see config.html from the base documentation
+
+=head1 VERSION
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+=cut
+
diff --git a/FS/bin/freeside-radgroup b/FS/bin/freeside-radgroup
new file mode 100644 (file)
index 0000000..ed85626
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/perl -w
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_svc;
+use FS::svc_acct;
+
+&untaint_argv;  #what it sounds like  (eww)
+
+my($user, $action, $groupname, $svcpart) = @ARGV;
+
+adminsuidsetup $user;
+
+my @svc_acct = map { $_->svc_x } qsearch('cust_svc', { svcpart => $svcpart } );
+
+if ( lc($action) eq 'add' ) {
+  foreach my $svc_acct ( @svc_acct ) {
+    my @groups = $svc_acct->radius_groups;
+    next if grep { $_ eq $groupname } @groups;
+    push @groups, $groupname;
+    my %hash = $svc_acct->hash;
+    $hash{usergroup} = \@groups;
+    my $new = new FS::svc_acct \%hash;
+    my $error = $new->replace($svc_acct);
+    die $error if $error;
+  }
+} else {
+  die &usage;
+}
+
+# subroutines
+
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-radgroup user action groupname svcpart\n";
+}
+
+=head1 NAME
+
+freeside-radgroup - Command line utility to manipulate radius groups
+
+=head1 SYNOPSIS
+
+  freeside-addgroup user action groupname svcpart 
+
+=head1 DESCRIPTION
+
+  B<user> is a freeside user as added with freeside-adduser.
+
+  B<command> is the action to take.  Available actions are: I<add>
+
+  B<groupname> is the group to add (or remove, etc.)
+
+  B<svcpart> specifies which accounts will be updated.
+
+=head1 EXAMPLES
+
+freeside-radgroup freesideuser add groupname 3
+
+Adds I<groupname> to all accounts with service definition 3.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<freeside-adduser>, L<FS::svc_acct>, L<FS::part_svc>
+
+=cut
+
diff --git a/FS/bin/freeside-receivables-report b/FS/bin/freeside-receivables-report
new file mode 100755 (executable)
index 0000000..f3ad2a1
--- /dev/null
@@ -0,0 +1,217 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use Date::Parse;
+use Time::Local;
+use Getopt::Std;
+use Text::Template;
+use Net::SMTP;
+use Mail::Header;
+use Mail::Internet;
+use FS::Conf;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_main;
+
+
+&untaint_argv; #what it sounds like  (eww)
+use vars qw($opt_v $opt_p $opt_m $opt_e $opt_t $report_lines $report_template @buf $header);
+getopts("vpmet:");     #switches
+
+#we're at now now (and later).
+my($_date)= $^T;
+
+# Get the current month
+my ($sec,$min,$hour,$mday,$mon,$year) =
+       (localtime($_date) )[0,1,2,3,4,5]; 
+$mon++;
+$year += 1900;
+
+# Login to the database
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+# Get the needed configuration files
+my $conf = new FS::Conf;
+my $lpr = $conf->config('lpr');
+my $email = $conf->config('email');
+my $smtpmachine = $conf->config('smtpmachine');
+my $mail_sender = $conf->exists('invoice_from') ? $conf->config('invoice_from') :
+  'postmaster';
+my @report_template = $conf->config('report_template')
+  or die "cannot load config file report_template";
+$report_lines = 0;
+  foreach ( grep /report_lines\(\d+\)/, @report_template ) { #kludgy :/
+  /report_lines\((\d+)\)/;
+  $report_lines += $1;
+}
+die "no report_lines() functions in template?" unless $report_lines;
+$report_template = new Text::Template (
+  TYPE   => 'ARRAY',
+  SOURCE => [ map "$_\n", @report_template ],
+) or die "can't create new Text::Template object: $Text::Template::ERROR";
+
+
+my(@customers)=qsearch('cust_main',{});
+if (scalar(@customers) == 0)
+{
+       exit 1;
+}
+
+# Open print and email pipes
+# $lpr and opt_p for printing
+# $email and opt_m for email
+
+if ($lpr && $opt_p)
+{
+        open(LPR, "|$lpr");
+}
+
+if ($email && $opt_m)
+{
+  $ENV{MAILADDRESS} = $mail_sender;
+  $header = new Mail::Header ( [
+    "From: Account Processor",
+    "To: $email",
+    "Sender: $mail_sender",
+    "Reply-To: $mail_sender",
+    "Subject: Receivables",
+  ] );
+}
+
+my $total = 0;
+
+
+# Now I can start looping
+foreach my $customer (@customers)
+{
+  my $custnum = $customer->getfield('custnum');
+  my $first = $customer->getfield('first');
+  my $last = $customer->getfield('last');
+  my $company = $customer->getfield('company');
+  my $daytime = $customer->getfield('daytime');
+  my $balance = $customer->balance;
+
+
+  if ($balance != 0) {
+    $total += $balance;
+    push @buf, sprintf(qq{%8d %-32.32s %12s %9.2f},
+      $custnum,
+      $first . " " . $last . "   " . $company,
+      $daytime,
+      $balance);
+
+  }
+
+}
+
+push @buf, ('', sprintf(qq{%61s}, "========="), sprintf(qq{%61.2f}, $total));
+
+sub FS::receivables_report::_template::report_lines {
+  my $lines = shift;
+  map {
+    scalar(@buf) ? shift @buf : '' ;
+  }
+  ( 1 .. $lines );
+}
+
+$FS::receivables_report::_template::title = " R E C E I V A B L E S ";
+$FS::receivables_report::_template::title = $opt_t if $opt_t;
+$FS::receivables_report::_template::page = 1;
+$FS::receivables_report::_template::date = $_date;
+$FS::receivables_report::_template::date = $_date;
+$FS::receivables_report::_template::total_pages = 
+  int( scalar(@buf) / $report_lines);
+$FS::receivables_report::_template::total_pages++ if scalar(@buf) % $report_lines;
+
+my @report;
+while (@buf) {
+  push @report, split("\n", 
+    $report_template->fill_in( PACKAGE => 'FS::receivables_report::_template' )
+  );
+  $FS::receivables_report::_template::page++;
+}
+
+if ($opt_v) {
+  print map "$_\n", @report;
+}
+if($lpr && $opt_p)
+{
+  print LPR map "$_\n", @report;
+  print LPR "\f" if $opt_e;
+  close LPR || die "Could not close printer: $lpr\n";
+}
+if($email && $opt_m)
+{
+  my $message = new Mail::Internet (
+    'Header' => $header,
+    'Body' => [ (@report) ],
+  );
+  $!=0;
+  $message->smtpsend( Host => "$smtpmachine" )
+    or die "can't send report to $email via $smtpmachine: $!";
+}
+
+
+# subroutines
+
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    $ARGV[$_] =~ /^([\w\-\/ \.]*)$/ || die "Illegal argument \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-receivables-report [-v] [-p] [-e] user\n";
+}
+
+=head1 NAME
+
+freeside-receivables-report - Prints or emails outstanding receivables.
+
+=head1 SYNOPSIS
+
+  freeside-receivables-report [-v] [-p] [-m] [-e] [-t "title"] user
+
+=head1 DESCRIPTION
+
+Prints or emails outstanding receivables
+
+B<-v>: Verbose - Prints records to STDOUT.
+
+B<-p>: Print to printer lpr as found in the conf directory.
+
+B<-m>: Mail output to user found in the Conf email file.
+
+B<-e>: Print a final form feed to the printer.
+
+B<-t>: supply a title for the top of each page.
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+=head1 VERSION
+
+$Id: freeside-receivables-report,v 1.6 2002-09-09 22:57:34 ivan Exp $
+
+=head1 BUGS
+
+Yes..... Use at your own risk. No guarantees or warrantees of any
+kind apply to this program. Parts of this program are hacked from
+other GNU licensed software created mainly by Ivan Kohler.
+
+This is released under the GNU Public License. See www.gnu.org
+for more information regarding this license.
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, config.html from the base documentation
+
+=head1 AUTHOR
+
+Jeff Finucane <jeff@cmh.net>
+
+based on print-batch by Joel Griffiths <griff@aver-computer.com>
+
+=cut
+
diff --git a/FS/bin/freeside-reexport b/FS/bin/freeside-reexport
new file mode 100644 (file)
index 0000000..b5c50a4
--- /dev/null
@@ -0,0 +1,62 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export;
+use FS::svc_acct;
+use FS::cust_svc;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $export_x = shift or die &usage;
+my @part_export;
+if ( $export_x =~ /^(\d+)$/ ) {
+  @part_export = qsearchs('part_export', { exportnum=>$1 } )
+    or die "exportnum $export_x not found\n";
+} else {
+  @part_export = qsearch('part_export', { exporttype=>$export_x } )
+    or die "no exports of type $export_x found\n";
+}
+
+my $svc_something = shift or die &usage;
+my $svc_x;
+if ( $svc_something =~ /^(\d+)$/ ) {
+  my $cust_svc = qsearchs('cust_svc', { svcnum=>$1 } )
+    or die "svcnum $svc_something not found\n";
+  $svc_x = $cust_svc->svc_x;
+} else {
+  $svc_x = qsearchs('svc_acct', { username=>$svc_something } )
+    or die "username $svc_something not found\n";
+}
+
+foreach my $part_export ( @part_export ) {
+  my $error = $part_export->export_insert($svc_x);
+  die $error if $error;
+}
+
+
+sub usage {
+  die "Usage:\n\n  freeside-reexport user exportnum|exporttype svcnum|username\n";
+}
+
+=head1 NAME
+
+freeside-reexport - Command line tool to re-trigger export jobs for existing services
+
+=head1 SYNOPSIS
+
+  freeside-reexport user exportnum|exporttype svcnum|username
+
+=head1 DESCRIPTION
+
+  Re-queues the export job for the specified exportnum or exporttype(s) and
+  specified service (selected by svcnum or username).
+
+=head1 SEE ALSO
+
+L<freeside-sqlradius-reset>, L<FS::part_export>
+
+=cut
+
diff --git a/FS/bin/freeside-selfservice-server b/FS/bin/freeside-selfservice-server
new file mode 100644 (file)
index 0000000..264cbc5
--- /dev/null
@@ -0,0 +1,235 @@
+#!/usr/bin/perl -w
+#
+# freeside-selfservice-server
+
+# alas, much false laziness with freeside-queued and fs_signup_server.  at
+# least it is slated to replace fs_{signup,passwd,mailadmin}_server
+# should probably generalize the version in here, or better yet use
+# Proc::Daemon or somesuch
+
+use strict;
+use vars qw( $Debug %kids $kids $max_kids $shutdown $log_file $ssh_pid );
+use Fcntl qw(:flock);
+use POSIX qw(:sys_wait_h setsid);
+use IO::Handle;
+use IO::Select;
+use IO::File;
+use Storable qw(nstore_fd fd_retrieve);
+use Net::SSH qw(sshopen2);
+use FS::UID qw(adminsuidsetup forksuidsetup);
+use FS::ClientAPI;
+
+use FS::Conf;
+use FS::cust_bill;
+use FS::cust_pkg;
+
+$Debug = 2; # >= 2 will log packet contents, including potentially compromising
+            # information
+
+$shutdown = 0;
+$max_kids = '10'; #?
+$kids = 0;
+
+my $user = shift or die &usage;
+my $machine = shift or die &usage;
+my $pid_file = "/var/run/freeside-selfservice-server.$user.pid";
+#my $pid_file = "/var/run/freeside-selfservice-server.$user.pid"; $FS::UID::datasrc not posible, but should include machine name at least, hmm
+
+&init($user);
+
+my $conf = new FS::Conf;
+
+if ($conf->exists('selfservice_server-quiet')) {
+    $FS::cust_bill::quiet = 1;
+    $FS::cust_pkg::quiet = 1;
+}
+
+my $clientd = "/usr/local/sbin/freeside-selfservice-clientd"; #better name?
+
+my $warnkids=0;
+while (1) {
+  my($writer,$reader,$error) = (new IO::Handle, new IO::Handle, new IO::Handle);
+  warn "connecting to $machine\n" if $Debug;
+
+  $ssh_pid = sshopen2($machine,$reader,$writer,$clientd);
+
+#  nstore_fd(\*writer, {'hi'=>'there'});
+
+  warn "entering main loop\n" if $Debug;
+  my $undisp = 0;
+  my $s = IO::Select->new( $reader );
+  while (1) {
+
+    &reap_kids;
+
+    warn "waiting for packet from client\n" if $Debug && !$undisp;
+    $undisp = 1;
+    my @handles = $s->can_read(5);
+    unless ( @handles ) {
+      &shutdown if $shutdown;
+      next;
+    }
+
+    $undisp = 0;
+
+    warn "receiving packet from client\n" if $Debug;
+
+    my $packet = fd_retrieve($reader);
+    warn "packet received\n".
+         join('', map { " $_=>$packet->{$_}\n" } keys %$packet )
+      if $Debug > 1;
+
+    #prevent runaway forking
+    my $warnkids = 0;
+    while ( $kids >= $max_kids ) {
+      warn "WARNING: maximum $kids children reached\n" unless $warnkids++;
+      &reap_kids;
+      sleep 1;
+    }
+
+    warn "forking child\n" if $Debug;
+    defined( my $pid = fork ) or die "can't fork: $!";
+    if ( $pid ) {
+      $kids++;
+      $kids{$pid} = 1;
+      warn "child $pid spawned\n" if $Debug;
+    } else { #kid time
+
+      #get new db handle
+      $FS::UID::dbh->{InactiveDestroy} = 1;
+      forksuidsetup($user);
+
+      my $type = $packet->{_packet};
+      warn "calling $type handler\n" if $Debug; 
+      my $rv = eval { FS::ClientAPI->dispatch($type, $packet); };
+      if ( $@ ) {
+        warn my $error = "WARNING: error dispatching $type: $@";
+        $rv = { _error => $error };
+      }
+      $rv->{_token} = $packet->{_token}; #identifier
+
+      warn "sending response\n" if $Debug;
+      flock($writer, LOCK_EX) or die "FATAL: can't lock write stream: $!";
+      nstore_fd($rv, $writer) or die "FATAL: can't send response: $!";
+      $writer->flush or die "FATAL: can't flush: $!";
+      flock($writer, LOCK_UN) or die "WARNING: can't release write lock: $!";
+
+      warn "child exiting\n" if $Debug;
+      exit; #end-of-kid
+    }
+
+  }
+
+}
+
+###
+# utility subroutines
+###
+
+sub reap_kids {
+  #warn "reaping kids\n";
+  foreach my $pid ( keys %kids ) {
+    my $kid = waitpid($pid, WNOHANG);
+    if ( $kid > 0 ) {
+      $kids--;
+      delete $kids{$kid};
+    }
+  }
+  #warn "done reaping\n";
+}
+
+sub init {
+  my $user = shift;
+
+  chdir "/" or die "Can't chdir to /: $!";
+  open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
+  defined(my $pid = fork) or die "Can't fork: $!";
+  if ( $pid ) {
+    print "freeside-selfservice-server to $machine started with pid $pid\n"; #logging to $log_file
+    exit unless $pid_file;
+    my $pidfh = new IO::File ">$pid_file" or exit;
+    print $pidfh "$pid\n";
+    exit;
+  }
+
+#  sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; }
+#  #sub REAPER { my $pid = wait; $kids--; $SIG{CHLD} = \&REAPER; }
+#  $SIG{CHLD} =  \&REAPER;
+
+  $shutdown = 0;
+  $SIG{HUP} = sub { warn "SIGHUP received; shutting down\n"; $shutdown++; };
+  $SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $shutdown++; };
+  $SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $shutdown++; };
+  $SIG{QUIT} = sub { warn "SIGQUIT received; shutting down\n"; $shutdown++; };
+  $SIG{PIPE} = sub { warn "SIGPIPE received; shutting down\n"; $shutdown++; };
+
+  #false laziness w/freeside-queued
+  my $freeside_gid = scalar(getgrnam('freeside'))
+    or die "can't setgid to freeside group\n";
+  $) = $freeside_gid;
+  $( = $freeside_gid;
+  #if freebsd can't setuid(), presumably it can't setgid() either.  grr fleabsd
+  ($(,$)) = ($),$();
+  $) = $freeside_gid;
+
+  $> = $FS::UID::freeside_uid;
+  $< = $FS::UID::freeside_uid;
+  #freebsd is sofa king broken, won't setuid()
+  ($<,$>) = ($>,$<);
+  $> = $FS::UID::freeside_uid;
+  #eslaf
+
+  $ENV{HOME} = (getpwuid($>))[7]; #for ssh
+  adminsuidsetup $user;
+
+  #$log_file = "/usr/local/etc/freeside/selfservice.". $FS::UID::datasrc; #MACHINE NAME
+  $log_file = "/usr/local/etc/freeside/selfservice.$machine.log";
+
+  open STDOUT, '>/dev/null'
+                            or die "Can't write to /dev/null: $!";
+  setsid                  or die "Can't start a new session: $!";
+  open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
+
+  $SIG{__DIE__} = \&_die;
+  $SIG{__WARN__} = \&_logmsg;
+
+  warn "freeside-selfservice-server starting\n";
+
+}
+
+sub shutdown {
+  my $wait = 12; #wait up to 1 minute
+  while ( $kids > 0 && $wait-- ) {
+    warn "waiting for $kids children to terminate";
+    sleep 5;
+  }
+  warn "abandoning $kids children" if $kids;
+  kill 'TERM', $ssh_pid if $ssh_pid;
+  die "exiting";
+}
+
+sub _die {
+  my $msg = shift;
+  unlink $pid_file if -e $pid_file;
+  _logmsg($msg);
+}
+
+sub _logmsg {
+  chomp( my $msg = shift );
+  _do_logmsg( "[server] [". scalar(localtime). "] [$$] $msg\n" );
+}
+
+sub _do_logmsg {
+  chomp( my $msg = shift );
+  my $log = new IO::File ">>$log_file";
+  flock($log, LOCK_EX);
+  seek($log, 0, 2);
+  print $log "$msg\n";
+  flock($log, LOCK_UN);
+  close $log;
+}
+
+sub usage {
+  die "Usage:\n\n  fs_signup_server user machine\n";
+}
+
diff --git a/FS/bin/freeside-setinvoice b/FS/bin/freeside-setinvoice
new file mode 100644 (file)
index 0000000..708e2fa
--- /dev/null
@@ -0,0 +1,42 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw(qsearch qsearchs);
+use FS::cust_main;
+use FS::svc_acct;
+
+&untaint_argv;  #what it sounds like  (eww)
+my $user = shift or die &usage;
+
+adminsuidsetup $user;
+
+foreach my $cust_main (
+   grep { ! scalar($_->invoicing_list) }
+     qsearch( 'cust_main', {} )
+) {
+  my @dest;
+  my @cust_pkg = $cust_main->ncancelled_pkgs;
+  foreach my $cust_pkg ( @cust_pkg ) {
+    foreach my $cust_svc ( $cust_pkg->cust_svc ) {
+      my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $cust_svc->svcnum } );
+      push @dest, $svc_acct->svcnum if $svc_acct;
+    }
+  }
+  push @dest, 'POST' unless @dest;
+  $cust_main->invoicing_list(\@dest);
+}
+
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    $ARGV[$_] =~ /^(.*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-setinvoice user\n";
+}
+
+
diff --git a/FS/bin/freeside-setup b/FS/bin/freeside-setup
new file mode 100755 (executable)
index 0000000..734744e
--- /dev/null
@@ -0,0 +1,1130 @@
+#!/usr/bin/perl -Tw
+
+#to delay loading dbdef until we're ready
+BEGIN { $FS::Record::setup_hack = 1; }
+
+use strict;
+use vars qw($opt_s);
+use Getopt::Std;
+use DBI;
+use DBIx::DBSchema 0.21;
+use DBIx::DBSchema::Table;
+use DBIx::DBSchema::Column;
+use DBIx::DBSchema::ColGroup::Unique;
+use DBIx::DBSchema::ColGroup::Index;
+use FS::UID qw(adminsuidsetup datasrc checkeuid getsecrets);
+use FS::Record;
+use FS::cust_main_county;
+use FS::raddb;
+use FS::part_bill_event;
+
+die "Not running uid freeside!" unless checkeuid();
+
+my %attrib2db =
+  map { lc($FS::raddb::attrib{$_}) => $_ } keys %FS::raddb::attrib;
+
+getopts("s");
+my $user = shift or die &usage;
+getsecrets($user);
+
+#needs to match FS::Record
+my($dbdef_file) = "/usr/local/etc/freeside/dbdef.". datasrc;
+
+###
+
+#print "\nEnter the maximum username length: ";
+#my($username_len)=&getvalue;
+my $username_len = 32; #usernamemax config file
+
+#print "\n\n", <<END, ":";
+#Freeside tracks the RADIUS User-Name, check attribute Password and
+#reply attribute Framed-IP-Address for each user.  You can specify additional
+#check and reply attributes (or you can add them later with the
+#fs-radius-add-check and fs-radius-add-reply programs).
+#
+#First enter any additional RADIUS check attributes you need to track for each 
+#user, separated by whitespace.
+#END
+#my @check_attributes = map { $attrib2db{lc($_)} or die "unknown attribute $_"; }
+#                         split(" ",&getvalue);
+#
+#print "\n\n", <<END, ":";
+#Now enter any additional reply attributes you need to track for each user,
+#separated by whitespace.
+#END
+#my @attributes = map { $attrib2db{lc($_)} or die "unknown attribute $_"; }
+#                   split(" ",&getvalue);
+#
+#print "\n\n", <<END, ":";
+#Do you wish to enable the tracking of a second, separate shipping/service
+#address?
+#END
+#my $ship = &_yesno;
+#
+#sub getvalue {
+#  my($x)=scalar(<STDIN>);
+#  chop $x;
+#  $x;
+#}
+#
+#sub _yesno {
+#  print " [y/N]:";
+#  my $x = scalar(<STDIN>);
+#  $x =~ /^y/i;
+#}
+
+my @check_attributes = (); #add later
+my @attributes = (); #add later
+my $ship = $opt_s;
+
+###
+
+my($char_d) = 80; #default maxlength for text fields
+
+#my(@date_type)  = ( 'timestamp', '', ''     );
+my(@date_type)  = ( 'int', 'NULL', ''     );
+my(@perl_type) = ( 'text', 'NULL', ''  ); 
+my @money_type = ( 'decimal',   '', '10,2' );
+
+###
+# create a dbdef object from the old data structure
+###
+
+my(%tables)=&tables_hash_hack;
+
+#turn it into objects
+my($dbdef) = new DBIx::DBSchema ( map {  
+  my(@columns);
+  while (@{$tables{$_}{'columns'}}) {
+    my($name,$type,$null,$length)=splice @{$tables{$_}{'columns'}}, 0, 4;
+    push @columns, new DBIx::DBSchema::Column ( $name,$type,$null,$length );
+  }
+  DBIx::DBSchema::Table->new(
+    $_,
+    $tables{$_}{'primary_key'},
+    DBIx::DBSchema::ColGroup::Unique->new($tables{$_}{'unique'}),
+    DBIx::DBSchema::ColGroup::Index->new($tables{$_}{'index'}),
+    @columns,
+  );
+} (keys %tables) );
+
+my $cust_main = $dbdef->table('cust_main');
+unless ($ship) { #remove ship_ from cust_main
+  $cust_main->delcolumn($_) foreach ( grep /^ship_/, $cust_main->columns );
+} else { #add indices
+  push @{$cust_main->index->lol_ref},
+    map { [ "ship_$_" ] } qw( last company daytime night fax );
+}
+
+#add radius attributes to svc_acct
+
+my($svc_acct)=$dbdef->table('svc_acct');
+
+my($attribute);
+foreach $attribute (@attributes) {
+  $svc_acct->addcolumn ( new DBIx::DBSchema::Column (
+    'radius_'. $attribute,
+    'varchar',
+    'NULL',
+    $char_d,
+  ));
+}
+
+foreach $attribute (@check_attributes) {
+  $svc_acct->addcolumn( new DBIx::DBSchema::Column (
+    'rc_'. $attribute,
+    'varchar',
+    'NULL',
+    $char_d,
+  ));
+}
+
+#create history tables (false laziness w/create-history-tables)
+foreach my $table ( grep { ! /^h_/ } $dbdef->tables ) {
+  my $tableobj = $dbdef->table($table)
+    or die "unknown table $table";
+
+  die "unique->lol_ref undefined for $table"
+    unless defined $tableobj->unique->lol_ref;
+  die "index->lol_ref undefined for $table"
+    unless defined $tableobj->index->lol_ref;
+
+  my $h_tableobj = DBIx::DBSchema::Table->new( {
+    name        => "h_$table",
+    primary_key => 'historynum',
+    unique      => DBIx::DBSchema::ColGroup::Unique->new( [] ),
+    'index'     => DBIx::DBSchema::ColGroup::Index->new( [
+                     @{$tableobj->unique->lol_ref},
+                     @{$tableobj->index->lol_ref}
+                   ] ),
+    columns     => [
+                     DBIx::DBSchema::Column->new( {
+                       'name'    => 'historynum',
+                       'type'    => 'serial',
+                       'null'    => 'NOT NULL',
+                       'length'  => '',
+                       'default' => '',
+                       'local'   => '',
+                     } ),
+                     DBIx::DBSchema::Column->new( {
+                       'name'    => 'history_date',
+                       'type'    => 'int',
+                       'null'    => 'NULL',
+                       'length'  => '',
+                       'default' => '',
+                       'local'   => '',
+                     } ),
+                     DBIx::DBSchema::Column->new( {
+                       'name'    => 'history_user',
+                       'type'    => 'varchar',
+                       'null'    => 'NOT NULL',
+                       'length'  => '80',
+                       'default' => '',
+                       'local'   => '',
+                     } ),
+                     DBIx::DBSchema::Column->new( {
+                       'name'    => 'history_action',
+                       'type'    => 'varchar',
+                       'null'    => 'NOT NULL',
+                       'length'  => '80',
+                       'default' => '',
+                       'local'   => '',
+                     } ),
+                     map {
+                       my $column = $tableobj->column($_);
+
+                       #clone so as to not disturb the original
+                       $column = DBIx::DBSchema::Column->new( {
+                         map { $_ => $column->$_() }
+                           qw( name type null length default local )
+                       } );
+
+                       $column->type('int')
+                         if $column->type eq 'serial';
+                       #$column->default('')
+                       #  if $column->default =~ /^nextval\(/i;
+                       #( my $local = $column->local ) =~ s/AUTO_INCREMENT//i;
+                       #$column->local($local);
+                       $column;
+                     } $tableobj->columns
+                   ],
+  } );
+  $dbdef->addtable($h_tableobj);
+}
+
+#important
+$dbdef->save($dbdef_file);
+&FS::Record::reload_dbdef($dbdef_file);
+
+###
+# create 'em
+###
+
+my($dbh)=adminsuidsetup $user;
+
+#create tables
+$|=1;
+
+foreach my $statement ( $dbdef->sql($dbh) ) {
+  $dbh->do( $statement )
+    or die "CREATE error: ". $dbh->errstr. "\ndoing statement: $statement";
+}
+
+#not really sample data (and shouldn't default to US)
+
+#cust_main_county
+
+#USPS state codes
+foreach ( qw(
+AL AK AS AZ AR CA CO CT DC DE FM FL GA GU HI ID IL IN IA KS KY LA
+ME MH MD MA MI MN MS MO MT NC ND NE NH NJ NM NV NY MP OH OK OR PA PW PR RI 
+SC SD TN TX UT VT VI VA WA WV WI WY AE AA AP
+) ) {
+  my($cust_main_county)=new FS::cust_main_county({
+    'state' => $_,
+    'tax'   => 0,
+    'country' => 'US',
+  });  
+  my($error);
+  $error=$cust_main_county->insert;
+  die $error if $error;
+}
+
+#AU "offical" state codes ala mark.williamson@ebbs.com.au (Mark Williamson)
+foreach ( qw(
+VIC NSW NT QLD TAS ACT WA SA
+) ) {
+  my($cust_main_county)=new FS::cust_main_county({
+    'state' => $_,
+    'tax'   => 0,
+    'country' => 'AU',
+  });  
+  my($error);
+  $error=$cust_main_county->insert;
+  die $error if $error;
+}
+
+#ISO 2-letter country codes (same as country TLDs) except US and AU
+foreach ( qw(
+AF AL DZ AS AD AO AI AQ AG AR AM AW AT AZ BS BH BD BB BY BE BZ BJ BM BT BO
+BA BW BV BR IO BN BG BF BI KH CM CA CV KY CF TD CL CN CX CC CO KM CG CK CR CI
+HR CU CY CZ DK DJ DM DO TP EC EG SV GQ ER EE ET FK FO FJ FI FR FX GF PF TF GA
+GM GE DE GH GI GR GL GD GP GU GT GN GW GY HT HM HN HK HU IS IN ID IR IQ IE IL
+IT JM JP JO KZ KE KI KP KR KW KG LA LV LB LS LR LY LI LT LU MO MK MG MW MY MV
+ML MT MH MQ MR MU YT MX FM MD MC MN MS MA MZ MM NA NR NP NL AN NC NZ NI NE NG
+NU NF MP NO OM PK PW PA PG PY PE PH PN PL PT PR QA RE RO RU RW KN LC VC WS SM
+ST SA SN SC SL SG SK SI SB SO ZA GS ES LK SH PM SD SR SJ SZ SE CH SY TW TJ TZ
+TH TG TK TO TT TN TR TM TC TV UG UA AE GB UM UY UZ VU VA VE VN VG VI WF EH
+YE YU ZR ZM ZW
+) ) {
+  my($cust_main_county)=new FS::cust_main_county({
+    'tax'   => 0,
+    'country' => $_,
+  });  
+  my($error);
+  $error=$cust_main_county->insert;
+  die $error if $error;
+}
+
+#billing events
+foreach my $aref ( 
+  [ 'COMP', 'Comp invoice', '$cust_bill->comp();', 30, 'comp' ],
+  [ 'CARD', 'Batch card', '$cust_bill->batch_card();', 40, 'batch-card' ],
+  [ 'BILL', 'Send invoice', '$cust_bill->send();', 50, 'send' ],
+  [ 'DCRD', 'Send invoice', '$cust_bill->send();', 50, 'send' ],
+  [ 'DCHK', 'Send invoice', '$cust_bill->send();', 50, 'send' ],
+) {
+
+  my $part_bill_event = new FS::part_bill_event({
+    'payby' => $aref->[0],
+    'event' => $aref->[1],
+    'eventcode' => $aref->[2],
+    'seconds' => 0,
+    'weight' => $aref->[3],
+    'plan' => $aref->[4],
+  });
+  my($error);
+  $error=$part_bill_event->insert;
+  die $error if $error;
+
+}
+
+$dbh->commit or die $dbh->errstr;
+$dbh->disconnect or die $dbh->errstr;
+
+#print "Freeside database initialized sucessfully\n";
+
+sub usage {
+  die "Usage:\n  freeside-setup [ -s ] user\n"; 
+}
+
+###
+# Now it becomes an object.  much better.
+###
+sub tables_hash_hack {
+
+  #note that s/(date|change)/_$1/; to avoid keyword conflict.
+  #put a kludge in FS::Record to catch this or? (pry need some date-handling
+  #stuff anyway also)
+
+  my(%tables)=( #yech.}
+
+    'agent' => {
+      'columns' => [
+        'agentnum', 'serial',            '',     '',
+        'agent',    'varchar',           '',     $char_d,
+        'typenum',  'int',            '',     '',
+        'freq',     'int',       'NULL', '',
+        'prog',     @perl_type,
+      ],
+      'primary_key' => 'agentnum',
+      'unique' => [],
+      'index' => [ ['typenum'] ],
+    },
+
+    'agent_type' => {
+      'columns' => [
+        'typenum',   'serial',  '', '',
+        'atype',     'varchar', '', $char_d,
+      ],
+      'primary_key' => 'typenum',
+      'unique' => [],
+      'index' => [],
+    },
+
+    'type_pkgs' => {
+      'columns' => [
+        'typenum',   'int',  '', '',
+        'pkgpart',   'int',  '', '',
+      ],
+      'primary_key' => '',
+      'unique' => [ ['typenum', 'pkgpart'] ],
+      'index' => [ ['typenum'] ],
+    },
+
+    'cust_bill' => {
+      'columns' => [
+        'invnum',    'serial',  '', '',
+        'custnum',   'int',  '', '',
+        '_date',     @date_type,
+        'charged',   @money_type,
+        'printed',   'int',  '', '',
+        'closed',    'char', 'NULL', 1,
+      ],
+      'primary_key' => 'invnum',
+      'unique' => [],
+      'index' => [ ['custnum'], ['_date'] ],
+    },
+
+    'cust_bill_event' => {
+      'columns' => [
+        'eventnum',    'serial',  '', '',
+        'invnum',   'int',  '', '',
+        'eventpart',   'int',  '', '',
+        '_date',     @date_type,
+        'status', 'varchar', '', $char_d,
+        'statustext', 'text', 'NULL', '',
+      ],
+      'primary_key' => 'eventnum',
+      #no... there are retries now #'unique' => [ [ 'eventpart', 'invnum' ] ],
+      'unique' => [],
+      'index' => [ ['invnum'], ['status'] ],
+    },
+
+    'part_bill_event' => {
+      'columns' => [
+        'eventpart',    'serial',  '', '',
+        'payby',       'char',  '', 4,
+        'event',       'varchar',           '',     $char_d,
+        'eventcode',    @perl_type,
+        'seconds',     'int', 'NULL', '',
+        'weight',      'int', '', '',
+        'plan',       'varchar', 'NULL', $char_d,
+        'plandata',   'text', 'NULL', '',
+        'disabled',     'char', 'NULL', 1,
+      ],
+      'primary_key' => 'eventpart',
+      'unique' => [],
+      'index' => [ ['payby'] ],
+    },
+
+    'cust_bill_pkg' => {
+      'columns' => [
+        'pkgnum',  'int', '', '',
+        'invnum',  'int', '', '',
+        'setup',   @money_type,
+        'recur',   @money_type,
+        'sdate',   @date_type,
+        'edate',   @date_type,
+        'itemdesc', 'varchar', 'NULL', $char_d,
+      ],
+      'primary_key' => '',
+      'unique' => [],
+      'index' => [ ['invnum'] ],
+    },
+
+    'cust_bill_pkg_detail' => {
+      'columns' => [
+        'detailnum', 'serial', '', '',
+        'pkgnum',  'int', '', '',
+        'invnum',  'int', '', '',
+        'detail',  'varchar', '', $char_d,
+      ],
+      'primary_key' => 'detailnum',
+      'unique' => [],
+      'index' => [ [ 'pkgnum', 'invnum' ] ],
+    },
+
+    'cust_credit' => {
+      'columns' => [
+        'crednum',  'serial', '', '',
+        'custnum',  'int', '', '',
+        '_date',    @date_type,
+        'amount',   @money_type,
+        'otaker',   'varchar', '', 32,
+        'reason',   'text', 'NULL', '',
+        'closed',    'char', 'NULL', 1,
+      ],
+      'primary_key' => 'crednum',
+      'unique' => [],
+      'index' => [ ['custnum'] ],
+    },
+
+    'cust_credit_bill' => {
+      'columns' => [
+        'creditbillnum', 'serial', '', '',
+        'crednum',  'int', '', '',
+        'invnum',  'int', '', '',
+        '_date',    @date_type,
+        'amount',   @money_type,
+      ],
+      'primary_key' => 'creditbillnum',
+      'unique' => [],
+      'index' => [ ['crednum'], ['invnum'] ],
+    },
+
+    'cust_main' => {
+      'columns' => [
+        'custnum',  'serial',  '',     '',
+        'agentnum', 'int',  '',     '',
+#        'titlenum', 'int',  'NULL',   '',
+        'last',     'varchar', '',     $char_d,
+#        'middle',   'varchar', 'NULL', $char_d,
+        'first',    'varchar', '',     $char_d,
+        'ss',       'varchar', 'NULL', 11,
+        'company',  'varchar', 'NULL', $char_d,
+        'address1', 'varchar', '',     $char_d,
+        'address2', 'varchar', 'NULL', $char_d,
+        'city',     'varchar', '',     $char_d,
+        'county',   'varchar', 'NULL', $char_d,
+        'state',    'varchar', 'NULL', $char_d,
+        'zip',      'varchar', '',     10,
+        'country',  'char', '',     2,
+        'daytime',  'varchar', 'NULL', 20,
+        'night',    'varchar', 'NULL', 20,
+        'fax',      'varchar', 'NULL', 12,
+        'ship_last',     'varchar', 'NULL', $char_d,
+#        'ship_middle',   'varchar', 'NULL', $char_d,
+        'ship_first',    'varchar', 'NULL', $char_d,
+        'ship_company',  'varchar', 'NULL', $char_d,
+        'ship_address1', 'varchar', 'NULL', $char_d,
+        'ship_address2', 'varchar', 'NULL', $char_d,
+        'ship_city',     'varchar', 'NULL', $char_d,
+        'ship_county',   'varchar', 'NULL', $char_d,
+        'ship_state',    'varchar', 'NULL', $char_d,
+        'ship_zip',      'varchar', 'NULL', 10,
+        'ship_country',  'char', 'NULL', 2,
+        'ship_daytime',  'varchar', 'NULL', 20,
+        'ship_night',    'varchar', 'NULL', 20,
+        'ship_fax',      'varchar', 'NULL', 12,
+        'payby',    'char', '',     4,
+        'payinfo',  'varchar', 'NULL', $char_d,
+        #'paydate',  @date_type,
+        'paydate',  'varchar', 'NULL', 10,
+        'payname',  'varchar', 'NULL', $char_d,
+        'tax',      'char', 'NULL', 1,
+        'otaker',   'varchar', '',    32,
+        'refnum',   'int',  '',     '',
+        'referral_custnum', 'int',  'NULL', '',
+        'comments', 'text', 'NULL', '',
+      ],
+      'primary_key' => 'custnum',
+      'unique' => [],
+      #'index' => [ ['last'], ['company'] ],
+      'index' => [ ['last'], [ 'company' ], [ 'referral_custnum' ],
+                   [ 'daytime' ], [ 'night' ], [ 'fax' ],
+                 ],
+    },
+
+    'cust_main_invoice' => {
+      'columns' => [
+        'destnum',  'serial',  '',     '',
+        'custnum',  'int',  '',     '',
+        'dest',     'varchar', '',  $char_d,
+      ],
+      'primary_key' => 'destnum',
+      'unique' => [],
+      'index' => [ ['custnum'], ],
+    },
+
+    'cust_main_county' => { #county+state+country are checked off the
+                            #cust_main_county for validation and to provide
+                            # a tax rate.
+      'columns' => [
+        'taxnum',   'serial',   '',    '',
+        'state',    'varchar',  'NULL',    $char_d,
+        'county',   'varchar',  'NULL',    $char_d,
+        'country',  'char',  '', 2, 
+        'taxclass',   'varchar', 'NULL', $char_d,
+        'exempt_amount', @money_type,
+        'tax',      'real',  '',    '', #tax %
+        'taxname',  'varchar',  'NULL',    $char_d,
+      ],
+      'primary_key' => 'taxnum',
+      'unique' => [],
+  #    'unique' => [ ['taxnum'], ['state', 'county'] ],
+      'index' => [],
+    },
+
+    'cust_pay' => {
+      'columns' => [
+        'paynum',   'serial',    '',   '',
+        #now cust_bill_pay #'invnum',   'int',    '',   '',
+        'custnum',  'int',    '',   '',
+        'paid',     @money_type,
+        '_date',    @date_type,
+        'payby',    'char',   '',     4, # CARD/BILL/COMP, should be index into
+                                         # payment type table.
+        'payinfo',  'varchar',   'NULL', $char_d,  #see cust_main above
+        'paybatch', 'varchar',   'NULL', $char_d, #for auditing purposes.
+        'closed',    'char', 'NULL', 1,
+      ],
+      'primary_key' => 'paynum',
+      'unique' => [],
+      'index' => [ [ 'custnum' ], [ 'paybatch' ] ],
+    },
+
+    'cust_bill_pay' => {
+      'columns' => [
+        'billpaynum', 'serial',     '',   '',
+        'invnum',  'int',     '',   '',
+        'paynum',  'int',     '',   '',
+        'amount',  @money_type,
+        '_date',   @date_type
+      ],
+      'primary_key' => 'billpaynum',
+      'unique' => [],
+      'index' => [ [ 'paynum' ], [ 'invnum' ] ],
+    },
+
+    'cust_pay_batch' => { #what's this used for again?  list of customers
+                          #in current CARD batch? (necessarily CARD?)
+      'columns' => [
+        'paybatchnum',   'serial',    '',   '',
+        'invnum',   'int',    '',   '',
+        'custnum',   'int',    '',   '',
+        'last',     'varchar', '',     $char_d,
+        'first',    'varchar', '',     $char_d,
+        'address1', 'varchar', '',     $char_d,
+        'address2', 'varchar', 'NULL', $char_d,
+        'city',     'varchar', '',     $char_d,
+        'state',    'varchar', 'NULL', $char_d,
+        'zip',      'varchar', '',     10,
+        'country',  'char', '',     2,
+#        'trancode', 'int', '', '',
+        'cardnum',  'varchar', '',     16,
+        #'exp',      @date_type,
+        'exp',      'varchar', '',     11,
+        'payname',  'varchar', 'NULL', $char_d,
+        'amount',   @money_type,
+      ],
+      'primary_key' => 'paybatchnum',
+      'unique' => [],
+      'index' => [ ['invnum'], ['custnum'] ],
+    },
+
+    'cust_pkg' => {
+      'columns' => [
+        'pkgnum',    'serial',    '',   '',
+        'custnum',   'int',    '',   '',
+        'pkgpart',   'int',    '',   '',
+        'otaker',    'varchar', '', 32,
+        'setup',     @date_type,
+        'bill',      @date_type,
+        'last_bill', @date_type,
+        'susp',      @date_type,
+        'cancel',    @date_type,
+        'expire',    @date_type,
+        'manual_flag', 'char', 'NULL', 1,
+      ],
+      'primary_key' => 'pkgnum',
+      'unique' => [],
+      'index' => [ ['custnum'] ],
+    },
+
+    'cust_refund' => {
+      'columns' => [
+        'refundnum',    'serial',    '',   '',
+        #now cust_credit_refund #'crednum',      'int',    '',   '',
+        'custnum',  'int',    '',   '',
+        '_date',        @date_type,
+        'refund',       @money_type,
+        'otaker',       'varchar',   '',   32,
+        'reason',       'varchar',   '',   $char_d,
+        'payby',        'char',   '',     4, # CARD/BILL/COMP, should be index
+                                             # into payment type table.
+        'payinfo',      'varchar',   'NULL', $char_d,  #see cust_main above
+        'paybatch',     'varchar',   'NULL', $char_d,
+        'closed',    'char', 'NULL', 1,
+      ],
+      'primary_key' => 'refundnum',
+      'unique' => [],
+      'index' => [],
+    },
+
+    'cust_credit_refund' => {
+      'columns' => [
+        'creditrefundnum', 'serial',     '',   '',
+        'crednum',  'int',     '',   '',
+        'refundnum',  'int',     '',   '',
+        'amount',  @money_type,
+        '_date',   @date_type
+      ],
+      'primary_key' => 'creditrefundnum',
+      'unique' => [],
+      'index' => [ [ 'crednum', 'refundnum' ] ],
+    },
+
+
+    'cust_svc' => {
+      'columns' => [
+        'svcnum',    'serial',    '',   '',
+        'pkgnum',    'int',    'NULL',   '',
+        'svcpart',   'int',    '',   '',
+      ],
+      'primary_key' => 'svcnum',
+      'unique' => [],
+      'index' => [ ['svcnum'], ['pkgnum'], ['svcpart'] ],
+    },
+
+    'part_pkg' => {
+      'columns' => [
+        'pkgpart',    'serial',    '',   '',
+        'pkg',        'varchar',   '',   $char_d,
+        'comment',    'varchar',   '',   $char_d,
+        'setup',      @perl_type,
+        'freq',       'int', '', '',  #billing frequency (months)
+        'recur',      @perl_type,
+        'setuptax',  'char', 'NULL', 1,
+        'recurtax',  'char', 'NULL', 1,
+        'plan',       'varchar', 'NULL', $char_d,
+        'plandata',   'text', 'NULL', '',
+        'disabled',   'char', 'NULL', 1,
+        'taxclass',   'varchar', 'NULL', $char_d,
+      ],
+      'primary_key' => 'pkgpart',
+      'unique' => [],
+      'index' => [ [ 'disabled' ], ],
+    },
+
+#    'part_title' => {
+#      'columns' => [
+#        'titlenum',   'int',    '',   '',
+#        'title',      'varchar',   '',   $char_d,
+#      ],
+#      'primary_key' => 'titlenum',
+#      'unique' => [ [] ],
+#      'index' => [ [] ],
+#    },
+
+    'pkg_svc' => {
+      'columns' => [
+        'pkgpart',    'int',    '',   '',
+        'svcpart',    'int',    '',   '',
+        'quantity',   'int',    '',   '',
+      ],
+      'primary_key' => '',
+      'unique' => [ ['pkgpart', 'svcpart'] ],
+      'index' => [ ['pkgpart'] ],
+    },
+
+    'part_referral' => {
+      'columns' => [
+        'refnum',   'serial',    '',   '',
+        'referral', 'varchar',   '',   $char_d,
+      ],
+      'primary_key' => 'refnum',
+      'unique' => [],
+      'index' => [],
+    },
+
+    'part_svc' => {
+      'columns' => [
+        'svcpart',    'serial',    '',   '',
+        'svc',        'varchar',   '',   $char_d,
+        'svcdb',      'varchar',   '',   $char_d,
+        'disabled',   'char',  'NULL',   1,
+      ],
+      'primary_key' => 'svcpart',
+      'unique' => [],
+      'index' => [ [ 'disabled' ] ],
+    },
+
+    'part_svc_column' => {
+      'columns' => [
+        'columnnum',   'serial',         '', '',
+        'svcpart',     'int',         '', '',
+        'columnname',  'varchar',     '', 64,
+        'columnvalue', 'varchar', 'NULL', $char_d,
+        'columnflag',  'char',    'NULL', 1, 
+      ],
+      'primary_key' => 'columnnum',
+      'unique' => [ [ 'svcpart', 'columnname' ] ],
+      'index' => [ [ 'svcpart' ] ],
+    },
+
+    #(this should be renamed to part_pop)
+    'svc_acct_pop' => {
+      'columns' => [
+        'popnum',    'serial',    '',   '',
+        'city',      'varchar',   '',   $char_d,
+        'state',     'varchar',   '',   $char_d,
+        'ac',        'char',   '',   3,
+        'exch',      'char',   '',   3,
+        'loc',       'char',   'NULL',   4, #NULL for legacy purposes
+      ],
+      'primary_key' => 'popnum',
+      'unique' => [],
+      'index' => [ [ 'state' ] ],
+    },
+
+    'part_pop_local' => {
+      'columns' => [
+        'localnum',  'serial',     '',     '',
+        'popnum',    'int',     '',     '',
+        'city',      'varchar', 'NULL', $char_d,
+        'state',     'char',    'NULL', 2,
+        'npa',       'char',    '',     3,
+        'nxx',       'char',    '',     3,
+      ],
+      'primary_key' => 'localnum',
+      'unique' => [],
+      'index' => [ [ 'npa', 'nxx' ], [ 'popnum' ] ],
+    },
+
+    'svc_acct' => {
+      'columns' => [
+        'svcnum',    'int',    '',   '',
+        'username',  'varchar',   '',   $username_len, #unique (& remove dup code)
+        '_password', 'varchar',   '',   50, #13 for encryped pw's plus ' *SUSPENDED* (mp5 passwords can be 34)
+        'sec_phrase', 'varchar',  'NULL',   $char_d,
+        'popnum',    'int',    'NULL',   '',
+        'uid',       'int', 'NULL',   '',
+        'gid',       'int', 'NULL',   '',
+        'finger',    'varchar',   'NULL',   $char_d,
+        'dir',       'varchar',   'NULL',   $char_d,
+        'shell',     'varchar',   'NULL',   $char_d,
+        'quota',     'varchar',   'NULL',   $char_d,
+        'slipip',    'varchar',   'NULL',   15, #four TINYINTs, bah.
+        'seconds',   'int', 'NULL',   '', #uhhhh
+        'domsvc',    'int', '',   '',
+      ],
+      'primary_key' => 'svcnum',
+      #'unique' => [ [ 'username', 'domsvc' ] ],
+      'unique' => [],
+      'index' => [ ['username'], ['domsvc'] ],
+    },
+
+    #'svc_charge' => {
+    #  'columns' => [
+    #    'svcnum',    'int',    '',   '',
+    #    'amount',    @money_type,
+    #  ],
+    #  'primary_key' => 'svcnum',
+    #  'unique' => [ [] ],
+    #  'index' => [ [] ],
+    #},
+
+    'svc_domain' => {
+      'columns' => [
+        'svcnum',    'int',    '',   '',
+        'domain',    'varchar',    '',   $char_d,
+        'catchall',  'int', 'NULL',    '',
+      ],
+      'primary_key' => 'svcnum',
+      'unique' => [ ['domain'] ],
+      'index' => [],
+    },
+
+    'domain_record' => {
+      'columns' => [
+        'recnum',    'serial',     '',  '',
+        'svcnum',    'int',     '',  '',
+        #'reczone',   'varchar', '',  $char_d,
+        'reczone',   'varchar', '',  255,
+        'recaf',     'char',    '',  2,
+        'rectype',   'varchar',    '',  5,
+        #'recdata',   'varchar', '',  $char_d,
+        'recdata',   'varchar', '',  255,
+      ],
+      'primary_key' => 'recnum',
+      'unique'      => [],
+      'index'       => [ ['svcnum'] ],
+    },
+
+    'svc_forward' => {
+      'columns' => [
+        'svcnum',   'int',    '',  '',
+        'srcsvc',   'int',    '',  '',
+        'dstsvc',   'int',    '',  '',
+        'dst',      'varchar',    'NULL',  $char_d,
+      ],
+      'primary_key' => 'svcnum',
+      'unique'      => [],
+      'index'       => [ ['srcsvc'], ['dstsvc'] ],
+    },
+
+    'svc_www' => {
+      'columns' => [
+        'svcnum',   'int',    '',  '',
+        'recnum',   'int',    '',  '',
+        'usersvc',  'int',    '',  '',
+      ],
+      'primary_key' => 'svcnum',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+    #'svc_wo' => {
+    #  'columns' => [
+    #    'svcnum',    'int',    '',   '',
+    #    'svcnum',    'int',    '',   '',
+    #    'svcnum',    'int',    '',   '',
+    #    'worker',    'varchar',   '',   $char_d,
+    #    '_date',     @date_type,
+    #  ],
+    #  'primary_key' => 'svcnum',
+    #  'unique' => [ [] ],
+    #  'index' => [ [] ],
+    #},
+
+    'prepay_credit' => {
+      'columns' => [
+        'prepaynum',   'serial',     '',   '',
+        'identifier',  'varchar', '', $char_d,
+        'amount',      @money_type,
+        'seconds',     'int',     'NULL', '',
+      ],
+      'primary_key' => 'prepaynum',
+      'unique'      => [ ['identifier'] ],
+      'index'       => [],
+    },
+
+    'port' => {
+      'columns' => [
+        'portnum',  'serial',     '',   '',
+        'ip',       'varchar', 'NULL', 15,
+        'nasport',  'int',     'NULL', '',
+        'nasnum',   'int',     '',   '',
+      ],
+      'primary_key' => 'portnum',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+    'nas' => {
+      'columns' => [
+        'nasnum',   'serial',     '',    '',
+        'nas',      'varchar', '',    $char_d,
+        'nasip',    'varchar', '',    15,
+        'nasfqdn',  'varchar', '',    $char_d,
+        'last',     'int',     '',    '',
+      ],
+      'primary_key' => 'nasnum',
+      'unique'      => [ [ 'nas' ], [ 'nasip' ] ],
+      'index'       => [ [ 'last' ] ],
+    },
+
+    'session' => {
+      'columns' => [
+        'sessionnum', 'serial',       '',   '',
+        'portnum',    'int',       '',   '',
+        'svcnum',     'int',       '',   '',
+        'login',      @date_type,
+        'logout',     @date_type,
+      ],
+      'primary_key' => 'sessionnum',
+      'unique'      => [],
+      'index'       => [ [ 'portnum' ] ],
+    },
+
+    'queue' => {
+      'columns' => [
+        'jobnum', 'serial', '', '',
+        'job', 'text', '', '',
+        '_date', 'int', '', '',
+        'status', 'varchar', '', $char_d,
+        'statustext', 'text', 'NULL', '',
+        'svcnum', 'int', 'NULL', '',
+      ],
+      'primary_key' => 'jobnum',
+      'unique'      => [],
+      'index'       => [ [ 'svcnum' ], [ 'status' ] ],
+    },
+
+    'queue_arg' => {
+      'columns' => [
+        'argnum', 'serial', '', '',
+        'jobnum', 'int', '', '',
+        'arg', 'text', 'NULL', '',
+      ],
+      'primary_key' => 'argnum',
+      'unique'      => [],
+      'index'       => [ [ 'jobnum' ] ],
+    },
+
+    'queue_depend' => {
+      'columns' => [
+        'dependnum', 'serial', '', '',
+        'jobnum', 'int', '', '',
+        'depend_jobnum', 'int', '', '',
+      ],
+      'primary_key' => 'dependnum',
+      'unique'      => [],
+      'index'       => [ [ 'jobnum' ], [ 'depend_jobnum' ] ],
+    },
+
+    'export_svc' => {
+      'columns' => [
+        'exportsvcnum' => 'serial', '', '',
+        'exportnum'    => 'int', '', '',
+        'svcpart'      => 'int', '', '',
+      ],
+      'primary_key' => 'exportsvcnum',
+      'unique'      => [ [ 'exportnum', 'svcpart' ] ],
+      'index'       => [ [ 'exportnum' ], [ 'svcpart' ] ],
+    },
+
+    'part_export' => {
+      'columns' => [
+        'exportnum', 'serial', '', '',
+        #'svcpart',   'int', '', '',
+        'machine', 'varchar', '', $char_d,
+        'exporttype', 'varchar', '', $char_d,
+        'nodomain',     'char', 'NULL', 1,
+      ],
+      'primary_key' => 'exportnum',
+      'unique'      => [],
+      'index'       => [ [ 'machine' ], [ 'exporttype' ] ],
+    },
+
+    'part_export_option' => {
+      'columns' => [
+        'optionnum', 'serial', '', '',
+        'exportnum', 'int', '', '',
+        'optionname', 'varchar', '', $char_d,
+        'optionvalue', 'text', 'NULL', '',
+      ],
+      'primary_key' => 'optionnum',
+      'unique'      => [],
+      'index'       => [ [ 'exportnum' ], [ 'optionname' ] ],
+    },
+
+    'radius_usergroup' => {
+      'columns' => [
+        'usergroupnum', 'serial', '', '',
+        'svcnum',       'int', '', '',
+        'groupname',    'varchar', '', $char_d,
+      ],
+      'primary_key' => 'usergroupnum',
+      'unique'      => [],
+      'index'       => [ [ 'svcnum' ], [ 'groupname' ] ],
+    },
+
+    'msgcat' => {
+      'columns' => [
+        'msgnum', 'serial', '', '',
+        'msgcode', 'varchar', '', $char_d,
+        'locale', 'varchar', '', 16,
+        'msg', 'text', '', '',
+      ],
+      'primary_key' => 'msgnum',
+      'unique'      => [ [ 'msgcode', 'locale' ] ],
+      'index'       => [],
+    },
+
+    'cust_tax_exempt' => {
+      'columns' => [
+        'exemptnum', 'serial', '', '',
+        'custnum',   'int', '', '',
+        'taxnum',    'int', '', '',
+        'year',      'int', '', '',
+        'month',     'int', '', '',
+        'amount',   @money_type,
+      ],
+      'primary_key' => 'exemptnum',
+      'unique'      => [ [ 'custnum', 'taxnum', 'year', 'month' ] ],
+      'index'       => [],
+    },
+
+    'router' => {
+      'columns' => [
+        'routernum', 'serial', '', '',
+        'routername', 'varchar', '', $char_d,
+        'svcnum', 'int', '0', '',
+      ],
+      'primary_key' => 'routernum',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+    'part_svc_router' => {
+      'columns' => [
+        'svcpart', 'int', '', '',
+       'routernum', 'int', '', '',
+      ],
+      'primary_key' => '',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+    'part_router_field' => {
+      'columns' => [
+        'routerfieldpart', 'serial', '', '',
+        'name', 'varchar', '', $char_d,
+       'length', 'int', '', '',
+       'check_block', 'text', 'NULL', '',
+       'list_source', 'text', 'NULL', '',
+      ],
+      'primary_key' => 'routerfieldpart',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+    'router_field' => {
+      'columns' => [
+        'routerfieldpart', 'int', '', '',
+        'routernum', 'int', '', '',
+        'value', 'varchar', '', 128,
+      ],
+      'primary_key' => '',
+      'unique'      => [ [ 'routerfieldpart', 'routernum' ] ],
+      'index'       => [],
+    },
+
+    'addr_block' => {
+      'columns' => [
+        'blocknum', 'serial', '', '',
+       'routernum', 'int', '', '',
+        'ip_gateway', 'varchar', '', 15,
+        'ip_netmask', 'int', '', '',
+      ],
+      'primary_key' => 'blocknum',
+      'unique'      => [ [ 'blocknum', 'routernum' ] ],
+      'index'       => [],
+    },
+
+    'part_sb_field' => {
+      'columns' => [
+        'sbfieldpart', 'serial', '', '',
+       'svcpart', 'int', '', '',
+       'name', 'varchar', '', $char_d,
+       'length', 'int', '', '',
+       'check_block', 'text', 'NULL', '',
+       'list_source', 'text', 'NULL', '',
+      ],
+      'primary_key' => 'sbfieldpart',
+      'unique'      => [ [ 'sbfieldpart', 'svcpart' ] ],
+      'index'       => [],
+    },
+
+    'sb_field' => {
+      'columns' => [
+        'sbfieldpart', 'int', '', '',
+       'svcnum', 'int', '', '',
+       'value', 'varchar', '', 128,
+      ],
+      'primary_key' => '',
+      'unique'      => [ [ 'sbfieldpart', 'svcnum' ] ],
+      'index'       => [],
+    },
+
+    'svc_broadband' => {
+      'columns' => [
+        'svcnum', 'int', '', '',
+        'blocknum', 'int', '', '',
+        'speed_up', 'int', '', '',
+        'speed_down', 'int', '', '',
+        'ip_addr', 'varchar', '', 15,
+      ],
+      'primary_key' => 'svcnum',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+  );
+
+  %tables;
+
+}
+
diff --git a/FS/bin/freeside-sqlradius-radacctd b/FS/bin/freeside-sqlradius-radacctd
new file mode 100644 (file)
index 0000000..4e8d57c
--- /dev/null
@@ -0,0 +1,180 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use vars qw( $log_file $sigterm $sigint );
+use subs qw( _die _logmsg );
+use Fcntl qw(:flock);
+use POSIX qw(setsid);
+use Date::Format;
+use IO::File;
+use FS::UID qw(adminsuidsetup);
+#use FS::Record qw(qsearch qsearchs);
+#use FS::part_export;
+#use FS::svc_acct;
+#use FS::cust_svc;
+
+#lots of false laziness w/freeside-queued
+
+my $user = shift or die &usage;
+
+#my $pid_file = "/var/run/freeside-sqlradius-radacctd.$user.pid";
+my $pid_file = "/var/run/freeside-sqlradius-radacctd.pid";
+
+&daemonize1;
+
+#sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; $kids--; }
+#$SIG{CHLD} =  \&REAPER;
+
+$sigterm = 0;
+$sigint = 0;
+$SIG{INT} = sub { warn "SIGINT received; shutting down\n"; $sigint++; };
+$SIG{TERM} = sub { warn "SIGTERM received; shutting down\n"; $sigterm++; };
+
+my $freeside_gid = scalar(getgrnam('freeside'))
+  or die "can't setgid to freeside group\n";
+$) = $freeside_gid;
+$( = $freeside_gid;
+#if freebsd can't setuid(), presumably it can't setgid() either.  grr fleabsd
+($(,$)) = ($),$();
+$) = $freeside_gid;
+
+$> = $FS::UID::freeside_uid;
+$< = $FS::UID::freeside_uid;
+#freebsd is sofa king broken, won't setuid()
+($<,$>) = ($>,$<);
+$> = $FS::UID::freeside_uid;
+
+#$ENV{HOME} = (getpwuid($>))[7]; #for ssh
+adminsuidsetup $user;
+
+$log_file= "/usr/local/etc/freeside/sqlradius-radacctd-log.". $FS::UID::datasrc;
+
+&daemonize2;
+
+$SIG{__DIE__} = \&_die;
+$SIG{__WARN__} = \&_logmsg;
+
+warn "freeside-sqlradius-radacctd starting\n";
+
+#eslaf
+
+#my $machine = shift or die &usage; #would need to be up higher for real
+my @exports = qsearch('part_export', { 'exporttype' => 'sqlradius' } );
+
+while (1) {
+
+  my %seen = ();
+  foreach my $export ( @exports ) {
+    next if $seen{$export->option('datasrc')}++;
+    my $dbh = DBI->connect(
+      map { $export->option($_) } qw( datasrc username password )
+    ) or do {
+      warn "can't connect to ". $export->option('datasrc'). ": ". $DBI::errstr;
+      next;
+    }
+
+    # find old radacct position
+    #$lastid = 0;
+
+    # get new radacct records
+    my $sth = $dbh->prepare('SELECT * FROM radacct WHERE radacctid > ?') or do {
+      warn "can't select in radacct table from ". $export->option('datasrc').
+           ": ". $dbh->errstr;
+      next;
+    };
+
+    while ( my $radacct = $sth->fetchrow_arrayref({}) ) {
+
+      my $session = new FS::session {
+        portnum =>
+        svcnum  => 
+        login   =>
+        #logout  =>
+      };
+
+    }
+
+    # look for updated radacct records & replace them
+
+  }
+
+  sleep 5;
+
+}
+
+#more false laziness w/freeside-queued
+
+sub usage {
+  die "Usage:\n\n  freeside-sqlradius-radacctd user\n";
+}
+
+sub _die {
+  my $msg = shift;
+  unlink $pid_file if -e $pid_file;
+  _logmsg($msg);
+}
+
+sub _logmsg {
+  chomp( my $msg = shift );
+  my $log = new IO::File ">>$log_file";
+  flock($log, LOCK_EX);
+  seek($log, 0, 2);
+  print $log "[". time2str("%a %b %e %T %Y",time). "] [$$] $msg\n";
+  flock($log, LOCK_UN);
+  close $log;
+}
+
+sub daemonize1 {
+
+  chdir "/" or die "Can't chdir to /: $!";
+  open STDIN, '/dev/null'   or die "Can't read /dev/null: $!";
+  defined(my $pid = fork) or die "Can't fork: $!";
+  if ( $pid ) {
+    print "freeside-sqlradius-radacctd started with pid $pid\n";
+          #logging to $log_file\n";
+    exit unless $pid_file;
+    my $pidfh = new IO::File ">$pid_file" or exit;
+    print $pidfh "$pid\n";
+    exit;
+  }
+  #open STDOUT, '>/dev/null'
+  #                          or die "Can't write to /dev/null: $!";
+  #setsid                  or die "Can't start a new session: $!";
+  #open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
+
+}
+
+sub daemonize2 {
+  open STDOUT, '>/dev/null'
+                            or die "Can't write to /dev/null: $!";
+  setsid                  or die "Can't start a new session: $!";
+  open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
+}
+
+
+#eslaf
+
+=head1 NAME
+
+freeside-sqlradius-radacctd - Real-time radacct import daemon
+
+=head1 SYNOPSIS
+
+  freeside-sqlradius-radacctd username
+
+=head1 DESCRIPTION
+
+Imports records from an SQL radacct table in real-time into the session
+monitor.
+
+This enables per-minute or per-hour charges as well as the
+"View active NAS ports" function.
+
+B<username> is a username added by freeside-adduser.
+
+=head1 SEE ALSO
+
+session.html from the base documentation.
+
+=cut
+
diff --git a/FS/bin/freeside-sqlradius-reset b/FS/bin/freeside-sqlradius-reset
new file mode 100755 (executable)
index 0000000..74f90a5
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export;
+use FS::svc_acct;
+use FS::cust_svc;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+#my $machine = shift or die &usage;
+
+my @exports =  qsearch('part_export', { exporttype=>'sqlradius' } );
+push @exports, qsearch('part_export', { exporttype=>'sqlradius_withdomain' } );
+
+
+foreach my $export ( @exports ) {
+  my $icradius_dbh = DBI->connect(
+    map { $export->option($_) } qw( datasrc username password )
+  ) or die $DBI::errstr;
+  for my $table (qw( radcheck radreply usergroup )) {
+    my $sth = $icradius_dbh->prepare("DELETE FROM $table");
+    $sth->execute or die "Can't reset $table table: ". $sth->errstr;
+  }
+  $icradius_dbh->disconnect;
+}
+
+foreach my $export ( @exports ) {
+
+  #my @svcparts = map { $_->svcpart } $export->export_svc;
+
+  my @svc_acct =
+    map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
+      map { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
+        grep { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
+          $export->export_svc;
+
+  foreach my $svc_acct ( @svc_acct ) {
+
+    #false laziness with FS::svc_acct::insert (like it matters)
+    my $error = $export->export_insert($svc_acct);
+    die $error if $error;
+
+  }
+}
+
+sub usage {
+  #die "Usage:\n\n  sqlradius_reset user machine\n";
+  die "Usage:\n\n  freeside-sqlradius-reset user\n";
+}
+
+=head1 NAME
+
+freeside-sqlradius-reset - Command line interface to reset and recreate RADIUS SQL tables
+
+=head1 SYNOPSIS
+
+  freeside-sqlradius-reset username
+
+=head1 DESCRIPTION
+
+Deletes the radcheck, radreply and usergroup tables and repopulates them from
+the Freeside database, for all sqlradius exports.
+
+B<username> is a username added by freeside-adduser.
+
+=head1 SEE ALSO
+
+L<freeside-reexport>, L<FS::part_export>, L<FS::part_export::sqlradius>
+
+=cut
+
+
+
diff --git a/FS/bin/freeside-sqlradius-seconds b/FS/bin/freeside-sqlradius-seconds
new file mode 100644 (file)
index 0000000..1c978fa
--- /dev/null
@@ -0,0 +1,58 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use Date::Parse;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearchs);
+use FS::svc_acct;
+
+my $fs_user = shift or die &usage;
+adminsuidsetup( $fs_user );
+
+my $target_user = shift or die &usage;
+my $start = shift or die &usage;
+$start = str2time($start);
+my $stop =  scalar(@ARGV) ? str2time(shift) : time;
+
+my $svc_acct = qsearchs( 'svc_acct', { 'username' => $target_user } );
+die "username $target_user not found\n" unless $svc_acct;
+
+print $svc_acct->seconds_since_sqlradacct( $start, $stop ). "\n";
+
+sub usage {
+  die "Usage:\n\n  freeside-sqlradius-seconds freeside_username target_username start_date stop_date\n";
+}
+
+
+=head1 NAME
+
+freeside-sqlradius-seconds - Real-time radacct import daemon
+
+=head1 SYNOPSIS
+
+  freeside-sqlradius-seconds freeside_username target_username start_date [ stop_date ]
+
+=head1 DESCRIPTION
+
+Returns the number of seconds the specified username has been online between
+start_date (inclusive) and stop_date (exclusive).
+See L<FS::svc_acct/seconds_since_sqlradacct>
+
+B<freeside_username> is a username added by freeside-adduser.
+B<target_username> is the username of the user account to query.
+B<start_date> and B<stop_date> are in any format Date::Parse is happy with.
+B<stop_date> defaults to now if not specified.
+
+=head1 BUGS
+
+Selection of the account in question is rather simplistic in that
+B<target_username> doesn't necessarily identify a unique account (and wouldn't
+even if a domain was specified), and no sqlradius export is checked for.
+
+=head1 SEE ALSO
+
+L<FS::svc_acct/seconds_since_sqlradacct>
+
+=cut
+
+1;
diff --git a/FS/bin/freeside-tax-report b/FS/bin/freeside-tax-report
new file mode 100755 (executable)
index 0000000..240f3ad
--- /dev/null
@@ -0,0 +1,292 @@
+#!/usr/bin/perl -Tw
+
+
+use strict;
+use Date::Parse;
+use Time::Local;
+use Getopt::Std;
+use Text::Template;
+use Net::SMTP;
+use Mail::Header;
+use Mail::Internet;
+use FS::Conf;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_bill;
+use FS::cust_bill_pay;
+use FS::cust_pay;
+
+
+&untaint_argv; #what it sounds like  (eww)
+use vars qw($opt_v $opt_p $opt_m $opt_e $opt_t $opt_s $opt_f $report_lines $report_template @buf $header);
+getopts("vpmef:s:");   #switches
+
+#we're at now now (and later).
+my($_finishdate)= $opt_f ? str2time($main::opt_f) : $^T;
+my($_startdate)= $opt_s ? str2time($main::opt_s) : $^T;
+
+# Get the current month
+my ($ssec,$smin,$shour,$smday,$smon,$syear) =
+       (localtime($_startdate) )[0,1,2,3,4,5]; 
+$smon++;
+$syear += 1900;
+
+# Get the current month
+my ($fsec,$fmin,$fhour,$fmday,$fmon,$fyear) =
+       (localtime($_finishdate) )[0,1,2,3,4,5]; 
+$fmon++;
+$fyear += 1900;
+
+# Login to the database
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+# Get the needed configuration files
+my $conf = new FS::Conf;
+my $lpr = $conf->config('lpr');
+my $email = $conf->config('email');
+my $smtpmachine = $conf->config('smtpmachine');
+my $mail_sender = $conf->exists('invoice_from') ? $conf->config('invoice_from') :
+  'postmaster';
+my @report_template = $conf->config('report_template')
+  or die "cannot load config file report_template";
+$report_lines = 0;
+foreach ( grep /report_lines\(\d+\)/, @report_template ) { #kludgy :/
+  /report_lines\((\d+)\)/;
+  $report_lines += $1;
+}
+die "no report_lines() functions in template?" unless $report_lines;
+$report_template = new Text::Template (
+  TYPE   => 'ARRAY',
+  SOURCE => [ map "$_\n", @report_template ],
+) or die "can't create new Text::Template object: $Text::Template::ERROR";
+
+
+my(@cust_bills)=qsearch('cust_bill',{});
+if (scalar(@cust_bills) == 0)
+{
+       exit 1;
+}
+
+# Open print and email pipes
+# $lpr and opt_p for printing
+# $email and opt_m for email
+
+if ($lpr && $main::opt_p)
+{
+        open(LPR, "|$lpr");
+}
+
+if ($email && $main::opt_m)
+{
+  $ENV{MAILADDRESS} = $mail_sender;
+  $header = new Mail::Header ( [
+    "From: Account Processor",
+    "To: $email",
+    "Sender: $mail_sender",
+    "Reply-To: $mail_sender",
+    "Subject: Sales Taxes Invoiced",
+  ] );
+}
+
+my $comped = 0;
+my $comped_tax = 0;
+my $other = 0;
+my $other_tax = 0;
+my $total = 0;
+my $taxed = 0;
+my $untaxed = 0;
+my $total_tax = 0;
+
+# Now I can start looping
+foreach my $cust_bill (@cust_bills)
+{
+       my $_date = $cust_bill->getfield('_date');
+       my $invnum = $cust_bill->getfield('invnum');
+       my $charged = $cust_bill->getfield('charged');
+
+       if ($_date >= $_startdate && $_date <= $_finishdate) {
+               $total += $charged;
+
+                # The following lines were used to produce rather verbose reports
+                #my ($sec,$min,$hour,$mday,$mon,$year) =
+                #       (localtime($_date) )[0,1,2,3,4,5]; 
+                #$mon++;
+                #$year -= 100 if $year >= 100;
+                #$year = "0" . $year if $year < 10;
+
+                my $invoice_amt =0;
+                my $invoice_tax =0;
+                my $invoice_comped =0;
+                my(@cust_bill_pkgs)= $cust_bill->cust_bill_pkg;
+                foreach my $cust_bill_pkg (@cust_bill_pkgs) {
+
+                        my $recur = $cust_bill_pkg->getfield('recur');
+                        my $setup = $cust_bill_pkg->getfield('setup');
+                        my $pkgnum = $cust_bill_pkg->getfield('pkgnum');
+                        
+                        if ($pkgnum == 0) {
+                                # The following line was used to produce rather verbose reports
+                                # push @buf, ('', sprintf(qq{%10s%15s%14.2f}, "$mon/$mday/$year", "Tax $invnum", $recur+$setup));
+                                $invoice_tax += $recur;
+                                $invoice_tax += $setup;
+                        } else {
+                                # The following line was used to produce rather verbose reports
+                                # push @buf, ('', sprintf(qq{%10s%15s%14.2f}, "$mon/$mday/$year", "Inv $invnum", $recur+$setup));
+                                $invoice_amt += $recur;
+                                $invoice_amt += $setup;
+                        }
+
+                }
+
+                my(@cust_bill_pays)= $cust_bill->cust_bill_pay;
+                foreach my $cust_bill_pay (@cust_bill_pays) {
+                        my $payby = $cust_bill_pay->cust_pay->payby;
+                        my $paid = $cust_bill_pay->getfield('amount');
+                        if ($payby =~ 'COMP') {
+                                $invoice_comped += $paid;
+                        }
+                }
+
+                if (abs($invoice_comped - ($invoice_amt + $invoice_tax)) < 0.0001){
+                        $comped += $invoice_amt;
+                        $comped_tax += $invoice_tax;
+                } elsif ($invoice_comped > 0) {
+                        push @buf, sprintf(qq{\nInvoice %10d has inexpliciable complimentary payments of %14.9f\n}, $invnum, $invoice_comped);
+                        $other += $invoice_amt;
+                        $other_tax += $invoice_tax;
+                } elsif ($invoice_tax > 0) {
+                        $total_tax += $invoice_tax;
+                        $taxed += $invoice_amt;
+                } else {
+                        $untaxed += $invoice_amt;
+                }
+
+        }
+
+}
+
+push @buf, ('', sprintf(qq{%25s%14.2f}, "Complimentary", $comped));
+push @buf, sprintf(qq{%25s%14.2f}, "Complimentary Tax", $comped_tax);
+push @buf, sprintf(qq{%25s%14.2f}, "Other", $other);
+push @buf, sprintf(qq{%25s%14.2f}, "Other Tax", $other_tax);
+push @buf, sprintf(qq{%25s%14.2f}, "Untaxed", $untaxed);
+push @buf, sprintf(qq{%25s%14.2f}, "Taxed", $taxed);
+push @buf, sprintf(qq{%25s%14.2f}, "Tax", $total_tax);
+push @buf, ('', sprintf(qq{%39s}, "========="), sprintf(qq{%39.2f}, $total));
+
+sub FS::tax_report::_template::report_lines {
+  my $lines = shift;
+  map {
+    scalar(@buf) ? shift @buf : '' ;
+  }
+  ( 1 .. $lines );
+}
+
+$FS::tax_report::_template::title = qq~SALES TAXES INVOICED for $smon/$smday/$syear through $fmon/$fmday/$fyear~;
+$FS::tax_report::_template::title = $opt_t if $opt_t;
+$FS::tax_report::_template::page = 1;
+$FS::tax_report::_template::date = $^T;
+$FS::tax_report::_template::date = $^T;
+$FS::tax_report::_template::fdate = $_finishdate;
+$FS::tax_report::_template::fdate = $_finishdate;
+$FS::tax_report::_template::sdate = $_startdate;
+$FS::tax_report::_template::sdate = $_startdate;
+$FS::tax_report::_template::total_pages = 
+  int( scalar(@buf) / $report_lines);
+$FS::tax_report::_template::total_pages++ if scalar(@buf) % $report_lines;
+
+my @report;
+while (@buf) {
+  push @report, split("\n", 
+    $report_template->fill_in( PACKAGE => 'FS::tax_report::_template' )
+  );
+  $FS::tax_report::_template::page++;
+}
+
+if ($opt_v) {
+  print map "$_\n", @report;
+}
+if($lpr && $opt_p)
+{
+  print LPR map "$_\n", @report;
+  print LPR "\f" if $opt_e;
+  close LPR || die "Could not close printer: $lpr\n";
+}
+if($email && $opt_m)
+{
+  my $message = new Mail::Internet (
+    'Header' => $header,
+    'Body' => [ (@report) ],
+  );
+  $!=0;
+  $message->smtpsend( Host => "$smtpmachine" )
+    or die "can't send report to $email via $smtpmachine: $!";
+}
+
+
+# subroutines
+sub untaint_argv {
+  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
+    $ARGV[$_] =~ /^([\w\-\/ :\.]*)$/ || die "Illegal argument \"$ARGV[$_]\"";
+    $ARGV[$_]=$1;
+  }
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-tax-report [-v] [-p] [-e] user\n";
+}
+
+=head1 NAME
+
+freeside-tax-report - Prints or emails sales taxes invoiced in a given period.
+
+=head1 SYNOPSIS
+
+  freeside-tax-report [-v] [-p] [-m] [-e] [-t "title"] [-s date] [-f date] user
+
+=head1 DESCRIPTION
+
+Prints or emails sales taxes invoiced in a given period.
+
+-v: Verbose - Prints records to STDOUT.
+
+-p: Print to printer lpr as found in the conf directory.
+
+-m: Email output to user found in the Conf email file.
+
+-e: Print a final form feed to the printer.
+
+-t: supply a title for the top of each page.
+
+-s: starting date for inclusion
+
+-f: final date for inclusion
+
+user: From the mapsecrets file - see config.html from the base documentation
+
+=head1 VERSION
+
+$Id: freeside-tax-report,v 1.5 2002-09-09 22:57:34 ivan Exp $
+
+=head1 BUGS
+
+Yes..... Use at your own risk. No guarantees or warrantees of any
+kind apply to this program. Parts of this program are hacked from
+other GNU licensed software created mainly by Ivan Kohler.
+
+This is released under the GNU Public License. See www.gnu.org
+for more information regarding this license.
+
+=head1 SEE ALSO
+
+L<FS::cust_main>, config.html from the base documentation
+
+=head1 AUTHOR
+
+Jeff Finucane <jeff@cmh.net>
+
+based on print-batch by Joel Griffiths <griff@aver-computer.com>
+
+=cut
+
diff --git a/FS/t/CGI.t b/FS/t/CGI.t
new file mode 100644 (file)
index 0000000..1b4e238
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::CGI;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/ClientAPI.t b/FS/t/ClientAPI.t
new file mode 100644 (file)
index 0000000..973d8da
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::ClientAPI;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/Conf.t b/FS/t/Conf.t
new file mode 100644 (file)
index 0000000..a9f7653
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::Conf;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/ConfItem.t b/FS/t/ConfItem.t
new file mode 100644 (file)
index 0000000..c7932d7
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::ConfItem;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/InitHandler.t b/FS/t/InitHandler.t
new file mode 100644 (file)
index 0000000..0ce60c8
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::InitHandler;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/Misc.t b/FS/t/Misc.t
new file mode 100644 (file)
index 0000000..cc7751a
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::Misc;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/Msgcat.t b/FS/t/Msgcat.t
new file mode 100644 (file)
index 0000000..29e71b3
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::Msgcat;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/Record.t b/FS/t/Record.t
new file mode 100644 (file)
index 0000000..00de1ed
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::Record;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/SearchCache.t b/FS/t/SearchCache.t
new file mode 100644 (file)
index 0000000..3c26f35
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::SearchCache;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/UID.t b/FS/t/UID.t
new file mode 100644 (file)
index 0000000..9f7da4e
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::UID;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/agent.t b/FS/t/agent.t
new file mode 100644 (file)
index 0000000..769cce2
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::agent;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/agent_type.t b/FS/t/agent_type.t
new file mode 100644 (file)
index 0000000..99c66a1
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::agent_type;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill.t b/FS/t/cust_bill.t
new file mode 100644 (file)
index 0000000..b43f08e
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_event.t b/FS/t/cust_bill_event.t
new file mode 100644 (file)
index 0000000..0e2ca3e
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_event;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_pay.t b/FS/t/cust_bill_pay.t
new file mode 100644 (file)
index 0000000..001eed0
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pay;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_pkg.t b/FS/t/cust_bill_pkg.t
new file mode 100644 (file)
index 0000000..0e45bdb
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pkg;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_bill_pkg_detail.t b/FS/t/cust_bill_pkg_detail.t
new file mode 100644 (file)
index 0000000..ea6e3d1
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pkg_detail;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_credit.t b/FS/t/cust_credit.t
new file mode 100644 (file)
index 0000000..cddf75c
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_credit;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_credit_bill.t b/FS/t/cust_credit_bill.t
new file mode 100644 (file)
index 0000000..0ef54c3
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_credit_bill;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_credit_refund.t b/FS/t/cust_credit_refund.t
new file mode 100644 (file)
index 0000000..6b2b599
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_credit_refund;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_main.t b/FS/t/cust_main.t
new file mode 100644 (file)
index 0000000..b0ffbdb
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_main;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_main_county.t b/FS/t/cust_main_county.t
new file mode 100644 (file)
index 0000000..dd61199
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_main_county;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_main_invoice.t b/FS/t/cust_main_invoice.t
new file mode 100644 (file)
index 0000000..9661620
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_main_invoice;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_pay.t b/FS/t/cust_pay.t
new file mode 100644 (file)
index 0000000..f6d0b75
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_pay;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_pay_batch.t b/FS/t/cust_pay_batch.t
new file mode 100644 (file)
index 0000000..02b572c
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_pay_batch;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_pkg.t b/FS/t/cust_pkg.t
new file mode 100644 (file)
index 0000000..c6a6860
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_pkg;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_refund.t b/FS/t/cust_refund.t
new file mode 100644 (file)
index 0000000..91583da
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_refund;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_svc.t b/FS/t/cust_svc.t
new file mode 100644 (file)
index 0000000..267d731
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_svc;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_tax_exempt.pm b/FS/t/cust_tax_exempt.pm
new file mode 100644 (file)
index 0000000..8af13e3
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_tax_exempt;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_tax_exempt.t b/FS/t/cust_tax_exempt.t
new file mode 100644 (file)
index 0000000..8af13e3
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_tax_exempt;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/domain_record.t b/FS/t/domain_record.t
new file mode 100644 (file)
index 0000000..794518c
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::domain_record;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/export_svc.t b/FS/t/export_svc.t
new file mode 100644 (file)
index 0000000..773c5de
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::export_svc;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/msgcat.t b/FS/t/msgcat.t
new file mode 100644 (file)
index 0000000..c38c639
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::msgcat;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/nas.t b/FS/t/nas.t
new file mode 100644 (file)
index 0000000..6f8ae36
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::nas;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_bill_event.t b/FS/t/part_bill_event.t
new file mode 100644 (file)
index 0000000..5626a9f
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_bill_event;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-apache.t b/FS/t/part_export-apache.t
new file mode 100644 (file)
index 0000000..b999508
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::apache;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-bind.t b/FS/t/part_export-bind.t
new file mode 100644 (file)
index 0000000..d0c96be
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::bind;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-bind_slave.t b/FS/t/part_export-bind_slave.t
new file mode 100644 (file)
index 0000000..c6a0386
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::bind_slave;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-bsdshell.t b/FS/t/part_export-bsdshell.t
new file mode 100644 (file)
index 0000000..eaf417a
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::bsdshell;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-cp.t b/FS/t/part_export-cp.t
new file mode 100644 (file)
index 0000000..bbefa6c
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::cp;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-cyrus.t b/FS/t/part_export-cyrus.t
new file mode 100644 (file)
index 0000000..e0b3f35
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::cyrus;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-domain_shellcommands.t b/FS/t/part_export-domain_shellcommands.t
new file mode 100644 (file)
index 0000000..a2a44fb
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::domain_shellcommands;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-forward_shellcommands.t b/FS/t/part_export-forward_shellcommands.t
new file mode 100644 (file)
index 0000000..78ca68d
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::forward_shellcommands;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-http.t b/FS/t/part_export-http.t
new file mode 100644 (file)
index 0000000..ea41b93
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::http;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-infostreet.t b/FS/t/part_export-infostreet.t
new file mode 100644 (file)
index 0000000..1b33418
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::infostreet;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-ldap.t b/FS/t/part_export-ldap.t
new file mode 100644 (file)
index 0000000..826c341
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::ldap;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-null.t b/FS/t/part_export-null.t
new file mode 100644 (file)
index 0000000..055cdce
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::null;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-shellcommands.t b/FS/t/part_export-shellcommands.t
new file mode 100644 (file)
index 0000000..7bb47d3
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::shellcommands;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-shellcommands_withdomain.t b/FS/t/part_export-shellcommands_withdomain.t
new file mode 100644 (file)
index 0000000..c0bd1bb
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::shellcommands_withdomain;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-sqlmail.t b/FS/t/part_export-sqlmail.t
new file mode 100644 (file)
index 0000000..b048a75
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::sqlmail;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-sqlradius.t b/FS/t/part_export-sqlradius.t
new file mode 100644 (file)
index 0000000..5fb23a5
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::sqlradius;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-sqlradius_withdomain.t b/FS/t/part_export-sqlradius_withdomain.t
new file mode 100644 (file)
index 0000000..504bf67
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::sqlradius_withdomain;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-sysvshell.t b/FS/t/part_export-sysvshell.t
new file mode 100644 (file)
index 0000000..7fc24ac
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::sysvshell;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-textradius.t b/FS/t/part_export-textradius.t
new file mode 100644 (file)
index 0000000..d8a48a0
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::textradius;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-vpopmail.t b/FS/t/part_export-vpopmail.t
new file mode 100644 (file)
index 0000000..2e37114
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::vpopmail;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export-www_shellcommands.t b/FS/t/part_export-www_shellcommands.t
new file mode 100644 (file)
index 0000000..2ea79cf
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export::www_shellcommands;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export.t b/FS/t/part_export.t
new file mode 100644 (file)
index 0000000..26b3987
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_export_option.t b/FS/t/part_export_option.t
new file mode 100644 (file)
index 0000000..13200c2
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_export_option;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_pkg.t b/FS/t/part_pkg.t
new file mode 100644 (file)
index 0000000..fd96073
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pkg;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_pop_local.t b/FS/t/part_pop_local.t
new file mode 100644 (file)
index 0000000..4e4ad17
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_pop_local;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_referral.t b/FS/t/part_referral.t
new file mode 100644 (file)
index 0000000..d20b979
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_referral;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_svc.t b/FS/t/part_svc.t
new file mode 100644 (file)
index 0000000..bdb2a7a
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_svc;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_svc_column.t b/FS/t/part_svc_column.t
new file mode 100644 (file)
index 0000000..467025c
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_svc_column;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/pkg_svc.t b/FS/t/pkg_svc.t
new file mode 100644 (file)
index 0000000..77d3429
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::pkg_svc;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/port.t b/FS/t/port.t
new file mode 100644 (file)
index 0000000..46377aa
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::port;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/prepay_credit.t b/FS/t/prepay_credit.t
new file mode 100644 (file)
index 0000000..e7626bd
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::prepay_credit;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/queue.t b/FS/t/queue.t
new file mode 100644 (file)
index 0000000..43e3373
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::queue;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/queue_arg.t b/FS/t/queue_arg.t
new file mode 100644 (file)
index 0000000..cf3f91d
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::queue_arg;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/queue_depend.t b/FS/t/queue_depend.t
new file mode 100644 (file)
index 0000000..8eaa2cd
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::queue_depend;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/raddb.t b/FS/t/raddb.t
new file mode 100644 (file)
index 0000000..ac28d07
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::raddb;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/radius_usergroup.t b/FS/t/radius_usergroup.t
new file mode 100644 (file)
index 0000000..325742c
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::radius_usergroup;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/session.t b/FS/t/session.t
new file mode 100644 (file)
index 0000000..c4b714e
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::session;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_Common.t b/FS/t/svc_Common.t
new file mode 100644 (file)
index 0000000..ed49e1e
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_Common;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_acct.t b/FS/t/svc_acct.t
new file mode 100644 (file)
index 0000000..9ca78c9
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_acct;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_acct_pop.t b/FS/t/svc_acct_pop.t
new file mode 100644 (file)
index 0000000..e612c40
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_acct_pop;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_domain.t b/FS/t/svc_domain.t
new file mode 100644 (file)
index 0000000..4d91898
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_domain;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_forward.t b/FS/t/svc_forward.t
new file mode 100644 (file)
index 0000000..d653d34
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_forward;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_www.t b/FS/t/svc_www.t
new file mode 100644 (file)
index 0000000..eb4e83f
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_www;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/type_pkgs.t b/FS/t/type_pkgs.t
new file mode 100644 (file)
index 0000000..9840180
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::type_pkgs;
+$loaded=1;
+print "ok 1\n";
diff --git a/INSTALL b/INSTALL
index ff2e43f..4b9b085 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -1 +1 @@
-See htdocs/docs/index.html
+See httemplate/docs/index.html
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..6256ccc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,212 @@
+#!/usr/bin/make
+
+DATASOURCE = DBI:Pg:dbname=freeside
+#DATASOURCE=DBI:mysql:freeside
+
+DB_USER = freeside
+DB_PASSWORD=
+
+TEMPLATE = asp
+#TEMPLATE = mason
+
+ASP_GLOBAL = /usr/local/etc/freeside/asp-global
+MASON_HANDLER = /usr/local/etc/freeside/handler.pl
+MASONDATA = /usr/local/etc/freeside/masondata
+
+#deb, others?
+FREESIDE_DOCUMENT_ROOT = /var/www/freeside
+#freebsd
+#FREESIDE_DOCUMENT_ROOT = /usr/local/www/data/freeside
+
+#deb, others?
+INIT_FILE = /etc/init.d/freeside
+#freebsd
+#INIT_FILE = /usr/local/etc/rc.d/011.freeside.sh
+
+#deb, others?
+HTTPD_RESTART = /etc/init.d/apache restart
+#freebsd
+#HTTPD_RESTART = /usr/local/etc/rc.d/apache.sh stop; sleep 1; /usr/local/etc/rc.d/apache.sh start
+
+FREESIDE_RESTART = ${INIT_FILE} restart
+
+#deb, others?
+INSTALLGROUP = root
+#freebsd
+#INSTALLGROUP = wheel
+
+#edit the stuff below to have the daemons start
+
+QUEUED_USER=fs_queue
+
+#eventually this shouldn't be needed
+FREESIDE_PATH = `pwd`
+
+PASSWD_USER = nostart
+PASSWD_MACHINE = localhost
+
+SIGNUP_USER = nostart
+SIGNUP_MACHINE = localhost
+SIGNUP_AGENTNUM = 2
+SIGNUP_REFNUM = 2
+
+SELFSERVICE_USER = fs_selfservice
+SELFSERVICE_MACHINE = localhost
+
+#---
+
+#not changable yet
+FREESIDE_CONF = /usr/local/etc/freeside
+
+VERSION=1.5.0pre3
+TAG=freeside_1_5_0pre3
+
+help:
+       @echo "supported targets: aspdocs masondocs alldocs docs install-docs"
+       @echo "                   htmlman"
+       @echo "                   perl-modules install-perl-modules"
+       @echo "                   install deploy"
+       @echo "                   create-database"
+       @echo "                   clean"
+
+aspdocs: htmlman httemplate/* httemplate/*/* httemplate/*/*/* httemplate/*/*/*/* httemplate/*/*/*/*/*
+       rm -rf aspdocs
+       cp -pr httemplate aspdocs
+       perl -p -i -e "\
+         s/%%%VERSION%%%/${VERSION}/g;\
+       " aspdocs/index.html
+       touch aspdocs
+
+
+masondocs: htmlman httemplate/* httemplate/*/* httemplate/*/*/* httemplate/*/*/*/* httemplate/*/*/*/*/*
+       rm -rf masondocs
+       cp -pr httemplate masondocs
+       ( cd masondocs; \
+         ../bin/masonize; \
+       )
+       perl -p -i -e "\
+         s/%%%VERSION%%%/${VERSION}/g;\
+       " masondocs/index.html
+       touch masondocs
+
+alldocs: aspdocs masondocs
+
+docs:
+       make ${TEMPLATE}docs
+
+htmlman:
+       [ -e ./httemplate/docs/man ] || mkdir httemplate/docs/man
+       [ -e ./httemplate/docs/man/bin ] || mkdir httemplate/docs/man/bin
+       [ -e ./httemplate/docs/man/FS ] || mkdir httemplate/docs/man/FS
+       [ -e ./httemplate/docs/man/FS/UI ] || mkdir httemplate/docs/man/FS/UI
+       [ -e ./httemplate/docs/man/FS/part_export ] || mkdir httemplate/docs/man/FS/part_export
+       chmod a+rx bin/pod2x
+       [ -e DONT_REBUILD_DOCS ] || bin/pod2x
+
+forcehtmlman:
+       [ -e ./httemplate/docs/man ] || mkdir httemplate/docs/man
+       [ -e ./httemplate/docs/man/bin ] || mkdir httemplate/docs/man/bin
+       [ -e ./httemplate/docs/man/FS ] || mkdir httemplate/docs/man/FS
+       [ -e ./httemplate/docs/man/FS/UI ] || mkdir httemplate/docs/man/FS/UI
+       [ -e ./httemplate/docs/man/FS/part_export ] || mkdir httemplate/docs/man/FS/part_export
+       bin/pod2x
+
+install-docs: docs
+       [ -e ${FREESIDE_DOCUMENT_ROOT} ] && mv ${FREESIDE_DOCUMENT_ROOT} ${FREESIDE_DOCUMENT_ROOT}.`date +%Y%m%d%H%M%S` || true
+       cp -r ${TEMPLATE}docs ${FREESIDE_DOCUMENT_ROOT}
+       [ "${TEMPLATE}" = "asp" -a ! -e ${ASP_GLOBAL} ] && mkdir ${ASP_GLOBAL} || true
+       [ "${TEMPLATE}" = "asp" ] && chown -R freeside ${ASP_GLOBAL} || true
+       [ "${TEMPLATE}" = "asp" ] && cp htetc/global.asa ${ASP_GLOBAL} || true
+       [ "${TEMPLATE}" = "mason" ] && cp htetc/handler.pl ${MASON_HANDLER} || true
+       [ "${TEMPLATE}" = "mason" -a ! -e ${MASONDATA} ] && mkdir ${MASONDATA} || true
+       [ "${TEMPLATE}" = "mason" ] && chown -R freeside ${MASONDATA} || true
+
+perl-modules:
+       cd FS; \
+       [ -e Makefile ] || perl Makefile.PL; \
+       make
+
+install-perl-modules: perl-modules
+       cd FS; \
+       make install UNINST=1
+
+install-init:
+       #[ -e ${INIT_FILE} ] || install -o root -g ${INSTALLGROUP} -m 711 init.d/freeside-init ${INIT_FILE}
+       install -o root -g ${INSTALLGROUP} -m 711 init.d/freeside-init ${INIT_FILE}
+       perl -p -i -e "\
+         s/%%%QUEUED_USER%%%/${QUEUED_USER}/g;\
+         s'%%%FREESIDE_PATH%%%'${FREESIDE_PATH}'g;\
+         s/%%%PASSWD_USER%%%/${PASSWD_USER}/g;\
+         s/%%%PASSWD_MACHINE%%%/${PASSWD_MACHINE}/g;\
+         s/%%%SIGNUP_USER%%%/${SIGNUP_USER}/g;\
+         s/%%%SIGNUP_MACHINE%%%/${SIGNUP_MACHINE}/g;\
+         s/%%%SIGNUP_AGENTNUM%%%/${SIGNUP_AGENTNUM}/g;\
+         s/%%%SIGNUP_REFNUM%%%/${SIGNUP_REFNUM}/g;\
+         s/%%%SELFSERVICE_USER%%%/${SELFSERVICE_USER}/g;\
+         s/%%%SELFSERVICE_MACHINE%%%/${SELFSERVICE_MACHINE}/g;\
+       " ${INIT_FILE}
+
+install: install-perl-modules install-docs install-init
+
+deploy: install
+       ${HTTPD_RESTART}
+       ${FREESIDE_RESTART}
+
+create-database:
+       perl -e 'use DBIx::DataSource qw( create_database ); create_database( "${DATASOURCE}", "${DB_USER}", "${DB_PASSWORD}" ) or die $$DBIx::DataSource::errstr;'
+
+create-config: install-perl-modules
+       [ -e ${FREESIDE_CONF} ] && mv ${FREESIDE_CONF} ${FREESIDE_CONF}.`date +%Y%m%d%H%M%S` || true
+       mkdir ${FREESIDE_CONF}
+       chown freeside ${FREESIDE_CONF}
+
+       touch ${FREESIDE_CONF}/secrets
+       chown freeside ${FREESIDE_CONF}/secrets
+       chmod 600 ${FREESIDE_CONF}/secrets
+
+       echo -e "${DATASOURCE}\n${DB_USER}\n${DB_PASSWORD}" >${FREESIDE_CONF}/secrets
+       chmod 600 ${FREESIDE_CONF}/secrets
+       chown freeside ${FREESIDE_CONF}/secrets
+
+       mkdir "${FREESIDE_CONF}/conf.${DATASOURCE}"
+       rm -rf conf/registries #old dirs just won't go away
+       cp conf/[a-z]* "${FREESIDE_CONF}/conf.${DATASOURCE}"
+       chown -R freeside "${FREESIDE_CONF}/conf.${DATASOURCE}"
+
+       mkdir "${FREESIDE_CONF}/counters.${DATASOURCE}"
+       chown freeside "${FREESIDE_CONF}/counters.${DATASOURCE}"
+
+       mkdir "${FREESIDE_CONF}/cache.${DATASOURCE}"
+       chown freeside "${FREESIDE_CONF}/cache.${DATASOURCE}"
+
+       mkdir "${FREESIDE_CONF}/export.${DATASOURCE}"
+       chown freeside "${FREESIDE_CONF}/export.${DATASOURCE}"
+
+clean:
+       rm -rf aspdocs masondocs
+       cd FS; \
+       make clean
+
+#these are probably only useful if you're me...
+
+upload-docs: forcehtmlman
+       ssh cleanwhisker.420.am rm -rf /var/www/www.sisd.com/freeside/devdocs
+       scp -pr httemplate/docs cleanwhisker.420.am:/var/www/www.sisd.com/freeside/devdocs
+
+release: upload-docs
+       cd /home/ivan/freeside
+       #cvs tag ${TAG}
+       cvs tag -F ${TAG}
+
+       #cd /home/ivan
+       cvs export -r ${TAG} -d freeside-${VERSION} freeside
+       tar czvf freeside-${VERSION}.tar.gz freeside-${VERSION}
+
+       scp freeside-${VERSION}.tar.gz ivan@cleanwhisker.420.am:/var/www/sisd.420.am/freeside/
+       mv freeside-${VERSION} freeside-${VERSION}.tar.gz ..
+
+update-webdemo:
+       ssh ivan@pouncequick.420.am '( cd freeside; cvs update -d -P )'
+       #ssh root@pouncequick.420.am '( cd /home/ivan/freeside; make clean; make deploy )'
+       ssh root@pouncequick.420.am '( cd /home/ivan/freeside; make deploy )'
+
diff --git a/README b/README
index 14234df..1030b38 100644 (file)
--- a/README
+++ b/README
@@ -1,43 +1,43 @@
-Freeside, (pre) 1.1.4
+Freeside
 
-Copyright (C) 1998 Silicon Interactive Software Design.  All rights reserved.
+Copyright (C) 2000,2001,2002,2003 Ivan Kohler
+Copyright (C) 1999 Silicon Interactive Software Design
+All rights reserved
 
     This program is free software; you can redistribute it and/or modify
-    it under the terms of either:
+    it under the terms of:
 
         a) the GNU General Public License as published by the Free
         Software Foundation; either version 2, or (at your option) any
-        later version, or
-
-        b) the "Artistic License"
+        later version
 
     This program is distributed in the hope that it will be useful,
     but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See either the
-    GNU General Public License or the Artistic License for more details.
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
   
     You should have received a copy of the GNU General Public
     License along with this program, in the file `GPL'; if not,
     write to the Free Software Foundation, Inc., 59 Temple Place - Suite
     330, Boston, MA 02111-1307, USA.
 
-    You should have received a copy of the Artistic License along with
-    this program, in the file `Artistic'; if not, download it from
-    http://www.perl.com/CPAN/doc/misc/license/Artistic
-
 Freeside is a billing and administration package for Internet Service 
 Providers.
 
 The Freeside home page is at `http://www.sisd.com/freeside'.
 
-The documentation is in `htdocs/docs'.
+The documentation is in `httemplate/docs'.
 
-A mailing list for users and developers is available.  Send a blank message to
+A mailing list for users is available.  Send a blank message to
 <ivan-freeside-subscribe@sisd.com> to subscribe.
 
-Commercial support is available from Ivan Kohler <ivan@sisd.com>.  Please
-subscribe to the the mailing list to request free support!
+A mailing list for developers is available.  It is intended to be lower volume
+and higher SNR than the users list.  Send a blank message to
+<ivan-freeside-devel-subscribe@sisd.com> to subscribe.
+
+Commercial support is available from Ivan Kohler <ivan@sisd.com>.  Requests for
+free support sent to me directly will be ignored.  Please subscribe to the the
+user mailing list to request free support!
 
-Ivan Kohler
-ivan@sisd.com
+Ivan Kohler <ivan-freeside_readme@420.am>
 
diff --git a/README.1.5.0pre1 b/README.1.5.0pre1
new file mode 100644 (file)
index 0000000..0de86bc
--- /dev/null
@@ -0,0 +1,17 @@
+preliminary upgrade instructions
+
+schema changes:
+  *** get svc_broadband changes from pc-intouch
+  *** otaker changes s/8/32 all otkaer fields
+  *** optional: sequence changes
+  *** add column cust_main_county.taxname
+  *** add column cust_bill_pkg.itemdesc
+  *** drop index cust_bill_pkg1
+  *** add index part_pkg1 and part_svc1
+
+install DBIx::DBSchema 0.21
+install NetAddr::IP
+
+Run dbdef-create
+something about history tables
+Restart apache and freeside-queued
diff --git a/TODO b/TODO
index 0171c32..4c582c9 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,530 +1,9 @@
-If you are interested in helping with any of these, please join the mailing
-list (send a blank message to ivan-freeside-subscribe@sisd.com) to avoid 
-duplication of effort.
+$Id: TODO,v 1.68 2002-02-16 18:14:23 ivan Exp $
 
--- 1.1.x --
-
-postgres can't deal with NULL!
-
-svc_acct.import should recognize "UNIX" in the RADIUS password file as null.
-
-radius logfile parsing and perl expression check.
-
-mailing list archive, faq, cvs
-
-(test cust_main.pm with cybercash v2 and v3)
-
-Fix in cust_bill BUGS: 
-There is an off-by-one error in print_text which causes a visual error (Page 1
-of 2 printed on some single-page invoices).
-
-FIX It doesn't properly inherit/override FS::Record yet, so no more replace vs
-rep silliness!
-
-fields should be a method against a FS::Record or derived object, as well as
-being something you can call as FS::Record::fields('tablename').  Might
-even be able to handle both in the same routine (that would be neato).
-Get rid of hfields and other assorted silliness.
-Clean up hfields/sfields/fields crap.  yuck.
-
-$lpr in cust_main.pm (from Bill.pm) should become /var/spool/freeside/conf/lpr
-
-Override FS::Record new, add, rep and del (create, insert, replace and
-delete) in all derived classes.
-IE create, insert, delete and replace from derived classes should override new, 
-add, del and rep (respectively) from FS::Record.  Depriciate old names.
-
-Allow a cancelled/suspended/active status from packages to bubble up to
-the customer lists.  Put active, then suspended, then cancelled accounts.
-Similar ordering on the package listing inside a single customer.
-
-Add the ability for services to filter information up to the package level
-for invoices and web screens, so you can select a particlar package based
-on username or domain name, etc.
-
-You can't delete the stuff under administration yet.  Add this,
-_including_ making sure the thing you are deleting is not in use!
-
-Immediate removal of incorrectly entered check payments (can't take too
-long to do this, or accounting is fubared).
-
-Add code to move from one service to another (POP to SLIP/PPP, etc.).
-This _should_ be possible by working off the rules in part_svc rather than
-hardcoding anything in.  The rules in part_svc may need some elaboration,
-perhaps.
-
-Use ut_ FS::Record methods in all derived classes (possibly some from dbdef?... eventually all from dbdef??? - but then `dbdef-create' would be impossible as there would be metadata we couldn't ask the backend for.  hmm.)
-
-(bring back from fsold, ) Generalize config-sending stuff and make more configurable.
-Expand the HylaFAX interface (also possibly generalize for other fax
-softwar ie .comfaxe); allow things like arbitrary faxes of sales
-literature, specific troubleshooting documents and so on.  Maybe even
-allow users to do this (though that might not belong in Freeside).
-misc/sendconfig.cgi
-misc/process/sendconfig.cgi
-Configure fax recipients via a separate box rather than using the finger
-name or first+last from cust_main.
-
-move all phone number logic out of Freeside - let HylaFAX or whatever
-handle it.
-
-soundex searches for customer name and company?  where are free soundex tools? (standard Text::Soundex duh)
-
-should be able to link on (username, domain name, some field in email alias) instead of svcnum only. (username done, what else?)
-
-(done but clean up) change svc_domain.pm mail sending from a pipe to "/usr/lib/sendmail" to Mail::Mailer or Net::SMTP or something.  also is the complete text of the registration agreement needed in there (it used to be)?
-
-generalize and make configurable new invoice printing scheme in FS::Bill::collect (past due)
-
-deleting an svc_domain should delete all associated svc_acct_sm records.
-same with a svc_acct.
-
-periodic password encrypter
-
-Automated, configurable notification, suspension and cancellation of
-defunct accounts.
-...
-expire cron job
-...
-Allow for a future setup date on accounts.
-
-one-time/per-customer/? changes in rates and descriptions ('remembered
-invoices'): implement by creating a new package on the fly... but it isn't 
-associated with any agent types so it won't show up for other customers to buy.
-
-if CGI::Base will not have redirect fixed (cgifix.html), should migrate to
-CGI.pm insetead?  It is >1 year newer.
-
-library repetitve stuff from Bill.pm Invoice.pm and friends (calculating
-previous balances etc etc)
-
-
-sub AUTOLOAD in FS::Record should warn? die? if used with a non-existant column
-name?
-
-edit (not just import, export and allow default/fixed) arbitrary radius stuff
-in svc_acct
-
-edit/svc_acct.cgi and edit/process/svc_acct.cgi should deal with arbitrary radius stuff
-
-radius import should take DEFAULT entry and put it in /var/spool/freeside/conf/radius-default ; svc_acct.export should use it (and doc)
-
-FS::Invoice and FS::Bill should merge with the classes they're derived from
-
-in UI, s/State/State\/Provence/go and s/County/County\/Locality/go
-
-.us domains and others!
-
-what else (besides l10n) for i18n?
-
-audit htdocs/* for things that should be libraried and things that should be
-new methods on the objects (need to do this before implementing a new UI)
-all the big things are done
-
-some places we die() where we should &FS::CGI::idiot (and perhaps vice-versa).
-Decide based on whether or not the "error" should show up in logs.
-
-all .cgi's should use standard header/footer and idiot() subroutines.  maybe HTML:: perl modules
-for HTML creation.  CGI.pm instead.
-
-library the conf reading stuff; bin/svc_acct.export version with missing-filename checking is good
-library conf stuff -> check all the conf stuff to make sure they close filehandles.
-
-When running bin/bill, Fix this (Annoying but harmless):
-Use of uninitialized value at /usr/local/lib/site_perl/FS/cust_pkg.pm line 99, <ADDRESS> chunk 4.
-Use of uninitialized value at /usr/local/lib/site_perl/FS/cust_pkg.pm line 102, <ADDRESS> chunk 4.
-Use of uninitialized value at /usr/local/lib/site_perl/FS/cust_pkg.pm line 105, <ADDRESS> chunk 4.
-
-all cgi (but internal to the isp) places where package names are listed should also have
-comment (like agent_type)
-
-clean up $recref and other silliness and use -> calls where possible, or
-one other alternative.  clean up everything else.
-should FS::Record use Tie::Hash?  That would be very clean, but where do we
-store the other information?  Maybe you could ask any FS::Record object for a
-tied hash?
-
-change all htdocs/edit/process/* loops to look like: (library this sort of thing!!!!)
-
-my($new) = create FS::svc_acct_sm ( {
-  map {
-    ($_, scalar($req->param($_)));
-  } qw(svcnum pkgnum svcpart domuser domuid domsvc)
-} );
-
-to avoid form errors causing too much silliness
-
-add this code to all svc_*.pm (already in acct and acct_sm and domain): (library!)
-
-  #get part_svc
-  my($svcpart);
-  my($svcnum)=$self->getfield('svcnum');
-  if ($svcnum) {
-    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum});
-    return "Unknown svcnum" unless $cust_svc; 
-    $svcpart=$cust_svc->svcpart;
-  } else {
-    $svcpart=$self->getfield('svcpart');
-  }
-  my($part_svc)=qsearchs('part_svc',{'svcpart'=>$svcpart});
-  return "Unkonwn svcpart" unless $part_svc;
-
-  #set fixed fields from part_svc
-  my($field);
-  foreach $field ( fields('svc_acct') ) {
-    if ( $part_svc->getfield('svc_acct__'. $field. '_flag') eq 'F' ) {
-      $self->setfield($field,$part_svc->getfield('svc_acct__'. $field) );
-    }
-  }
-
-change all file access from regular open(FILE,) stuff to OO, because of 
-problems scoping and passing filehandles like that.
-
-svc_domain.pm mail sending uses Date::Format which doesn't seem to pick up 
-correct timezone.
-
-view/svc_domain.cgi needs to know the domain might be unaudited (cosmetic)
-
-Check everything into CVS.
-
---- 1.1.x or 1.2 or later
-
-the web interface should create a new object and use it instead of a blank
-form for new records.  the create method of svc_ objects should set defaults
-(from part_svc).
-
-sub check in man FS::table_name should be rewriteen.  Get rid of $recref
-stuff.  Make sure all fields that refer to other database are checked.
-
-Integration with signup disks (are there any free ones?  Netscape?).
-
-One-button cancel (+refund) for lusers who can't get online.
-
-Keep information on virtual web servers (hostname, IP, host machine,
-directory, etc.) and export this information for importation into the ISPs
-web farm. 
-
-Remove requirement that the first mail alias be the catchall?  Still make
-sure only one catchall per domain is defined in any case, of course. 
-
-Ability to move cust_pkg records from one customer to another? (proably
-will need to cancel the old and create a new like when we move services
-between packages). 
-
-Auto-increment expired cards one year, and try again?
-
-Lay out the forms a bit better.
-
-More non-US stuff - zip codes, country codes, foreign currencies, etc.
-
-cust_refund.{cgi.pm} need to do cards xaxtions.  (now we only have cust_credit)
-
-Nicer set of integrated reporting possibilities, like weekly sales totals
-by customer, package, agent, referral, etc., aging reports sorted by lots
-of different things, and so on.
-
-Client/server setup for users to modify their own passwords, shells, etc,
-via passwd or secure web interface (prelminary passwd/chfn/chsh
-replacement done).  Complicated by the fact that we don't want to allow
-incoming connections to the machine running Freeside, so we probably need
-to have a daemon on each external shell or web machine that is contacted
-by the Freeside machine.  Be very very careful for both traditional
-security issues and DoS problems. 
-
-An extension of the above to allow users to modify selected parts of their
-own information, order and cancel services.  A web interface for new
-customers.
-
-Expand domain name stuff to house all domain information.  Export
-named.boot/named.conf (primary and secondary) and named.{domain} files.
-Add more registries (not just InterNIC's com org net edu)
-
-Nice postscript paper invoices, rather than current ASCII invoices.
-
-
-think about race-condititions in FS::Record and derived ->check ->insert
-and so on, uid and username checks in svc_acct, etc.
-
-Move to rsync over ssh file exportation rather than scp.
-
-check 'n fix the proactive password checker. (cracklib?)
-
-refunds of "BILL" payments: generate pseudo-check.
-
-write batch senders and batch parsers for the different credit card processors
-people use/
-More CC processors/methods.
-
-In FS::Record, the counter dir should have .datasrc appended to it like the 
-dbdef does, which should place all the (most of) the DB metadata in unique 
-files and let me run concurrent .datasrc's.  Maybe do something similar for 
-user, password and datasrc itself? (or something to get the out of the source
-files) and then we're set. (secrets file also needs .datasrc appended, or maybe
-"/var/spool/freeside".datasrc
-
-you should be able to fiddle the setup date in cust_pkg. (at least initially)
-
-cych v3 and v2 support
-
-delete options in administration section
-
-write a generic batch senders and batch parsers.
-
-need a way to override svc_acct export on a per-machine basis; just use config files based on machine name i suppose; document that.
-
-you should be able to get column types as a method against an FS::Record object
-as well as dbdef->table($table)->column($column)->type
-
-move to perl module for fuzzy and soundex searching.
-
-make fs-setup option to add sample data so you can click on "New Customer" right away?  so people understand what this stuff is?
-
-package view needs to list extraneous services; we need to prevent the
-creation of them so this never happens (and mark it as such in the source)
-(the creation problem should be fixed - though they will still happen if people
-fsck around in the data manually, so list them anyway)
-
-add attribute dictionary to fs-setup as a menu, plus analyze users file to
-decide automatically
-
-Check for and report on duplicate billing accounts (cust_main, though many
-will have a need for these so probably don't disable them outright.)
-
-create a ->warn as well as a ->check method for all FS::table classes?
-(see above)
-
-something to automate making a release and updating the web demo
-
-export a debian-style (also redhat and?) /etc/group file aswell!
-
-seems to be an off-by-one error in the ascii invoice formatting which is saying
-"1 of 2" pages when there is only one.
-
-get rid of agrep?  needs the (non-free) glimpse distribution.  agrep used to
-be free?  what else can do fuzzy searching?
-
-site_perl/svc_domain.cgi (hmm... or maybe should have a button?  or maybe svc_domain.pm should handle this) should set $whois_hack for non-internic domains, so you can add them...
-
-svc_acct_sm.import qmail import should pull in recipientmap people too.
-
-.pm's like svc_acct.pm which need to do time-consuming things like ssh remotely
-should fork and do them in a child.
-
-i18n/l10n: take ALL messages and catalog them in english.txt or in database or something, so we can eventually go int'l.  int'l currency support would be a help aswell.
-
-get some of { city, county, state, zip } from the missing bits if
-possible (where can i get the data to do this?  usps.gov?)
-
-additional interfaces (perltk?  java?)
-
-Put the GPL notice in all files.
-
--- 1.2 or later --
-
-$cust_bill->owed database field to be eliminated, replaced by a method call
-that calculates on the fly.  make sure to grep for ->(get|set)field('owed') 
-same for cust_credit->credited
-
-Export quota information.
-
-move all configuration to a central place.  maybe in blob's in the
-database.  maybe even things like the code to execute when a username is
-changed can be in there, so less of the distributed scripts change between
-different sites.
-
-Implement setup and recurring fees as Safe perl expressions rather than
-numbers, to allow for variable-rate services.  Backwards compatibility is
-obtained because { 43 } in perl is still 43.  :)  Define API to pass
-starting and ending dates and any other necessary data to expression
-(fees are currently evaluated as Safe expressions but more work needs to
-be done to define an opmask for various needs, write examples
-(usage-based billing, etc.) and so on).
-...
-Add the ability to modify the next billing date in cust_pkg, and take
-appropriate action.  This will allow the implementation of pro-rate/1st of
-the month billing as well as the ability to manually fiddle with
-anniversary dates in cust_pkg, so you can sync a customer's anniversary
-date even if you're using anniversary billing (manually or automatically).
-(now with above, we need to have a way to automatically pro-rate /^(\d+)$/
-charges - anything more complicated should figure it out itself given
-starting and ending dates [document that!])
-...
-Daily Radius log parsing into database; other logfile formats?
-...
-Callbacks to enforce hourly limits on accounts (suspend until the end of
-the billing period?), for those who limit customers rather than tack on
-extra charges.
-
-Flag packages (part_pkg) as taxable or non/taxable as some ISPs (for
-example) need to charge tax on equipment but not service (separate flags
-for setup and recurring fee... or perhaps a setup_tax, setup_notax,
-recur_tax and recur_notax fees, and possibly something more flexible if
-there is need).
-
-Allow for a variable number of invoices for customers who need multiple
-copies.
-
-Add a mail alias service with table svc_acct (not domain mail aliasing
-which is domain with svc_acct_sm)
-
-(bring back from fsold) Change customer comment field from its current kludge to something more
-workable.
-
-Better work orders with more information.  Should eventually open a ticket
-when we have such a thing.
-edit/svc_wo.cgi
-edit/process/svc_wo.cgi
-Call tracking and trouble tickets.
-
-use mod_perl and Apache::AuthDBI instead of mod_auth_mysql when we do local 
-users
-More accoutability for complimentary accounts: approval, expiration, term
-(no more than x months in advance) and notification.
-Flag particular users (or all users, for that matter) as having their
-passwords hidden and/or locked from users of Freeside (maybe need Freeside
-security levels first?). 
-...
-Better Freeside-level configurable access, for those ISP's who have
-employees they can't trust.  Right now you're "stuck" with setting up
-.htaccess stuff yourself.  This should really just be integrated. 
-
-update site_perl/table_template* (pry out of date)
-
-/var/spool/freeside/conf (and whatever else /var/spool/freeside we can)
-in database (except secrets), then web interface, 
-make /var/spool/freeside a configurable directory (probably as part of 
-some automated installation process?)
-
-add a table with column of export services (passwd, shadow, master.passwd, .qmail file update, dns update, etc.) and rows machine groups and whether or not to export that (and any necessary parameters).  wasn't matt (vunderkid, not matt@michweb) working on this?  find him?  each machine goes in a group of its own as well as a group based on function.  add a table with only svcpart and machine group.  now, when you import from each machine, it can get its own accounts with one svcpart and universal accounts with another svcpart.  (though that does make the username duplicate checking more interesting)
-
-password and slipip stuff in svc_acct.pm store need to be split into two fields or something, so the silliness in svc_acct.pm and svc_acct.export with looking at the data to decide what to do with it can be fixed (1.2)
-
-This requires some serious magic in FS::Record:
-ok, if date_type in fs-setup is to be something besides int,
-now we need to create wrappers
-for them so they behave identically across RDBMS's, ie date pops out as as
-UNIX timestamp (or an object of some sort? maybe even a blessed $obj which
-is a string not a hashref for backwards compatibility?) and so on. (remember
-to treat '0' as Not a Date instead of 1/1/70.
-
-Add Freeside-level transactions for RDBMS's which don't support
-transcations?  (Currently we assume a minimal RDBMS which has no rollback,
-transactions or atomic updates).  Or just require a RDBMS that supports
-rollback and/or atomic updates and get rid of the work-arounds?  The /rdb
-interface had this kludge on top of it but is a technical dead-end in most
-other ways, unless it can gain an SQL parser and DBD interface.
-
-Better automated comparison of our CC records with processors (CyberCash,
-at least, has not always had 100% accuracy, though recent versions are
-much better) 
-
-Expect or other pty based login check, where we actually connect to a
-terminal server or shell machine and test logging in as the user (if we
-are keeping a cleartext password for that user)  (This is something tech
-support often needs for new customers)
-
-Use cust_main table for pre-sales tracking as well?
-
-Automatic commision report and check generation via freq and prog (to
-become a Safe perl expression) fields in agent table, and possibly others.
-
-Database and add a mailed-out date and method for disk/CD mailing, so a
-customer can call and you can say, "sent on xx/xx/xx via {US Mail, Fedex,
-UPS, etc}" 
-
-Inventory tracking for physical items such as routers (for sale or
-lease... probably doesn't make a difference in the ordering... but if you
-cancel a router lease the inventory should come back.  hmm.)
-
--- Matt's wishlist ---
-
-From matt@michweb.net Fri Feb 20 16:39:53 1998
-Date: Thu, 19 Feb 1998 23:20:11 -0500
-From: Matt Simerson <matt@michweb.net>
-Reply-To: quadran-developer@netgoth.com
-To: quadran-developer@netgoth.com
-Subject: Re: Welcome to quadran-developer
-
->Whats it based on and what is it supposed to do?  I'm interested, but
->unfortunatly, I don't have that much time to help on the project (I'm busily
->working on one of my own based around MySQL and Qt right now -- don't know
->if it will be GPL'ed or not yet -- we'll probably just use it in house since
->it is designed around our system)...
-
-That's what I set out to find, but didn't find anything on the web site.
-I'm looking for something that will do the following:
-
-Single point of entry for users on a secure system:
-       Creates account on user (public) systems
-               update /etc/passwd/master.passwd file
-               update radius database (if necessary)
-       Set up up disk quotas (although I hacked adduser to do this)
-       Option for adding user to a mailing list(s)
-       Export of new user info to customizable report (for automated entry
-into
-               accounting software, etc...)
-
-Automated billing:
-       Export credit card info for batch processing and have hooks built
-               in for other forms of electronic processing.
-       Batch-Payment (apply payments from formatted text file).
-       Customizable reports for manual entry/importing into Accounting
-software
-       Email or laser print invoices
-       Sanity checks credit card numbers before processing (code available)
-
-Simple method for disabling an account.
-       Arbitrary Expiration Dates (on a given day, in x days)
-       Remove from radius.
-       Changing password to '*'
-       Virtual customers disabling dns, http server, log processing, etc..
-
-Billing for different account types:
-       Dialup monthly flat rate. Prorates for partial months.
-       Dialup monthly flat rate for x hours + hourly usage.
-       Dialup email only
-       Email only accounts
-       Virtual Web accounts - w/multiple mailboxes
-       Leased line accounts
-       Disk space used over quota.
-       Tech support minimum + hourly charges
-       Other for misc stuff (modem, RAM, etc...)
-
-Per user definable RADIUS attributes (ties in with above)
-       Fixed IP
-       Simultaneous Use
-       IP filters (for dialup email only)
-
-Keep logs of modem usage generated daily from radius accounting logs stored
-on multiple radius servers.
-
-Keep logs of disk usage generated from quota.
-
-Method of adding virtual domains to your system:
-       Automatically grabs an IP address from a preassigned pool.
-       Creates a domain.com database file from database fields
-       Updates /etc/named.conf or /etc/named.boot and reloads named.
-       Add's virtual.com to /etc/sendmail.cw or qmail control files.
-       Edits your web servers httpd.conf file and restarts http server.
-       An optional section for adding vif's can be added if the users OS
-               supports adding them on the fly. Otherwise it's up to the end
-               user. Make a hook that can run a custom script that the user
-               tweaks for his system.
-       Update or create the config file your web stats analyzer needs. I've
-               done this for analog (free) and http-analyze. Probably
-               should only officially support analog and let users hack
-               it to their hearts desire.
-I've already written scripts that do most of the virtual web stuff on my
-system...in bash. Shouldn't be hard for a perlmeister to convert. In fact,
-as long as all the info was stored in the database (username, domain name,
-and ip pool) this could easily just be run as an external script that the
-user tweaks to match his system.
-
-We use a great accounting software (M.Y.O.B) that does all the AP, AR,
-Payroll, Tax stuff, and most everything else we could need. It's already
-set up for the type of checks we have, etc, etc... I just need something to
-do the billing part. I can import/export sales and payments directly once
-the billing part is done. You can't write accounting software as good as
-M.Y.O.B. for $120.
+The TODO list / bug-tracking is now kept in a database.  See
+http://pouncequick.420.am/rt/
 
+If you are interested in helping with any of these, please join the
+*development* mailing list (send a blank message to
+ivan-freeside-devel-subscribe@sisd.com) to avoid duplication of effort.
 
diff --git a/bin/apache.export b/bin/apache.export
new file mode 100755 (executable)
index 0000000..f0a6bee
--- /dev/null
@@ -0,0 +1,65 @@
+#!/usr/bin/perl -w
+
+use strict;
+#use File::Path;
+use File::Rsync;
+use Net::SSH qw(ssh);
+use FS::UID qw(adminsuidsetup datasrc);
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export;
+use FS::cust_svc;
+use FS::svc_www;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+#needs the export number in there somewhere too...?
+my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/apache";
+mkdir $spooldir, 0700 unless -d $spooldir;
+
+my @exports = qsearch('part_export', { 'exporttype' => 'apache' } );
+
+my $rsync = File::Rsync->new({
+  rsh     => 'ssh',
+#  dry_run => 1,
+});
+
+foreach my $export ( @exports ) {
+
+  my $machine = $export->machine;
+  my $file = "$spooldir/$machine.conf";
+
+  open(HTTPD_CONF,">$file") or die "can't open $file: $!";
+
+  my $template = $export->option('template');
+
+  my @svc_www = $export->svc_x;
+
+  foreach my $svc_www ( @svc_www ) {
+    use vars qw($zone $username);
+    $zone = $svc_www->domain_record->zone;
+    $username = $svc_www->svc_acct->username;
+    print HTTPD_CONF eval(qq("$template")). "\n\n";
+  }
+
+  my $user = $export->option('user');
+  my $httpd_conf = $export->option('httpd_conf');
+
+  $rsync->exec( {
+    src       => $file,
+    dest      => "$user\@$machine:$httpd_conf",
+  } ) or die "rsync to $machine failed: ". join(" / ", $rsync->err);
+ # warn $rsync->out;
+
+  ssh("root\@$machine", 'apachectl graceful');
+
+}
+
+close HTTPD_CONF;
+
+# -----
+
+sub usage {
+  die "Usage:\n  apache.export user\n"; 
+}
+
diff --git a/bin/bill b/bin/bill
deleted file mode 100755 (executable)
index 5c5be70..0000000
--- a/bin/bill
+++ /dev/null
@@ -1,188 +0,0 @@
-#!/usr/local/bin/perl -Tw
-#
-# bill: Bill customer(s)
-#
-# Usage: bill [ -c [ i ] ] [ -d 'date' ] [ -b ]
-#
-# Bills all customers.
-#
-# Adds record to /dbin/cust_bill and /dbin/cust_pay (if payment made -
-# CARD & COMP), prints invoice / charges card etc.
-#
-# -c: Turn on collecting (you probably want this).
-#
-# -i: real-time billing (as opposed to batch billing).  only relevant
-#     for credit cards.
-#
-# -d: Pretent it's 'date'.  Date is in any format Date::Parse is happy with,
-#     but be careful.
-#
-# ## n/a ## -b: send batch when done billing
-#
-# ivan@voicenet.com sep/oct 96
-#
-# separated billing and collections, cleaned up code.
-# ivan@voicenet.com 96-nov-11
-#
-# added -d option
-# ivan@voicenet.com 96-nov-13
-#
-# added -v option and started to implement it, added 'd:' to getopts call
-#  (oops!)
-# ivan@voicenet.com 97-jan-2
-#
-# added more debug messages, moved some searches to fssearch.pl library (for 
-# speed)
-# rewrote "all customer" finder to know about bill dates, for speed.
-# ivan@voicenet.com 97-jan-8
-#
-# thought about it a while, and removed passing of the -d option to collect...?
-# ivan@voicenet.com 97-jan-14
-#
-# make all -v stuff STDERR 
-# ivan@voicenet.com 97-feb-4
-#
-# added pkgnum as argument to program from /db/part_pkg, with kludge for the
-# "/bin/echo XX" 's already there.
-# ivan@voicenet.com 97-feb-23
-#
-# - general cleanup
-# - customers who are suspended can still be billed for the setup fee
-# - cust_pkg record is re-read after the package setup fee program is run.
-#   this way,
-#   that program can modify the record (for example, to start accounts off
-#   suspended)
-#   (best to think four or five times before modifying anything else!)
-# ivan@voicenet.com 97-feb-26
-#
-# don't bill recurring fee if its not time! (was removed)
-# ivan@voicenet.com 97-mar-6
-#
-# added -b option, send batch when done billing.
-# ivan@voicenet.com 97-apr-4
-#
-#insecure dependency on line 179ish below needs to be fixed before bill is
-#used setuid
-# ivan@voicenet.com 97-jun-2
-#
-# removed running of setup program (depriciated)
-# ivan@voicenet.com 97-jul-21
-#
-# rewrote for new API, removed option to specify custnums (use FS::Bill 
-# instead), removed -v option (?)
-# ivan@voicenet.com 97-jul-22 - 23 - 25 -28
-# (need to add back in email stuff, look in /home/ivan/old/dbin/collect)
-#
-# s/suidsetup/adminsuidsetup/, s/FS::Search/FS::Record/, added some batch
-# exporting stuff (which still needs to be generalized) and removed &idiot
-# ivan@sisd.com 98-may-27
-
-# setup
-
-use strict;
-use Fcntl qw(:flock);
-use Date::Parse;
-use Getopt::Std;
-use FS::UID qw(adminsuidsetup swapuid);
-use FS::Record qw(qsearch qsearchs);
-use FS::Bill;
-
-my($batchfile)="/var/spool/freeside/batch";
-my($batchlock)="/var/spool/freeside/batch.lock";
-
-adminsuidsetup;
-
-&untaint_argv; #what it sounds like  (eww)
-use vars qw($opt_b $opt_c $opt_i $opt_d);
-getopts("bcid:");      #switches
-
-#we're at now now (and later).
-my($time)= $main::opt_d ? str2time($main::opt_d) : $^T;
-
-# find packages w/ bill < time && cancel != '', and create corresponding
-# customer objects
-
-my($cust_main,%saw);
-foreach $cust_main (
-  map {
-    if ( ( $_->getfield('bill') || 0 ) <= $time &&
-         !$saw{ $_->getfield('custnum') }++ ) {
-      qsearchs('cust_main',{'custnum'=> $_->getfield('custnum') } );
-    } else {
-      ();
-    }
-  } qsearch('cust_pkg',{'cancel'=>''})
-) {
-
-  # and bill them
-
-  print "Billing customer #" . $cust_main->getfield('custnum') . "\n";
-
-  bless($cust_main,"FS::Bill");
-
-  my($error);
-
-  $error=$cust_main->bill('time'=>$time);
-  warn "Error billing,  customer #" . $cust_main->getfield('custnum') . 
-    ":" . $error if $error;
-
-  if ($main::opt_c) {
-    $error=$cust_main->collect('invoice_time'=>$time,
-                               'batch_card' => $main::opt_i ? 'no' : 'yes',
-                              );
-    warn "Error collecting customer #" . $cust_main->getfield('custnum') .
-      ":" . $error if $error;
-
-  #sleep 1;
-
-  }
-
-}
-
-#if ($main::opt_b) {
-#
-#  die "Batch still waiting for reply? ($batchlock exists)\n" if -e $batchlock;
-#  open(BATCHLOCK,"+>>$batchlock") or die "Can't open $batchlock: $!";
-#  select(BATCHLOCK); $|=1; select(STDOUT);
-#  unless ( flock(BATCHLOCK,,LOCK_EX|LOCK_NB) ) {
-#    seek(BATCHLOCK,0,0);
-#    my($pid)=<BATCHLOCK>;
-#    chop($pid);
-#    die "Is a batch running? (pid $pid)\n";
-#  }
-#  seek(BATCHLOCK,0,0);
-#  print BATCHLOCK $$,"\n";
-#
-#  ( open(BATCH,">$batchfile")
-#    and flock(BATCH,LOCK_EX|LOCK_NB)
-#  ) or die "Can't open $batchfile: $!";
-#
-#  my($cust_pay_batch);
-#  foreach $cust_pay_batch (qsearch('cust_pay_batch',{})) {
-#    print BATCH join(':',
-#      $_->getfield('cardnum'),
-#      $_->getfield('exp'),
-#      $_->getfield('amount'),
-#      $_->getfield('payname')
-#        || $_->getfield('first'). ' '. $_->getfield('last'),
-#      "Description",
-#      $_->getfield('zip'),
-#    ),"\n";
-#  }
-#
-#  flock(BATCH,LOCK_UN);
-#  close BATCH;
-#
-#  flock(BATCHLOCK,LOCK_UN);
-#  close BATCHLOCK;
-#}
-
-# subroutines
-
-sub untaint_argv {
-  foreach $_ ( $[ .. $#ARGV ) { #untaint @ARGV
-    $ARGV[$_] =~ /^([\w\-\/]*)$/ || die "Illegal arguement \"$ARGV[$_]\"";
-    $ARGV[$_]=$1;
-  }
-}
-
diff --git a/bin/bind.export b/bin/bind.export
new file mode 100755 (executable)
index 0000000..64d4406
--- /dev/null
@@ -0,0 +1,190 @@
+#!/usr/bin/perl -w
+
+use strict;
+use File::Path;
+use File::Rsync;
+use Net::SSH qw(ssh);
+use FS::UID qw(adminsuidsetup datasrc);
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export;
+use FS::cust_pkg;
+use FS::cust_svc;
+use FS::svc_domain;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/bind";
+mkdir $spooldir, 0700 unless -d $spooldir;
+
+my @exports = qsearch('part_export', { 'exporttype' => 'bind' } );
+my @sexports = qsearch('part_export', { 'exporttype' => 'bind_slave' } );
+
+my $rsync = File::Rsync->new({
+  rsh     => 'ssh',
+#  dry_run => 1,
+});
+
+foreach my $export ( @exports ) {
+
+  my $machine = $export->machine;
+  my $prefix = "$spooldir/$machine";
+
+  my $bind_rel = $export->option('bind_release');
+  my $ndc_cmd = ($bind_rel eq 'BIND9') ? 'rndc' : 'ndc';
+  my $minttl = $export->option('bind9_minttl');
+
+  #prevent old domain files from piling up
+  #rmtree "$prefix" or die "can't rmtree $prefix.db: $!";
+
+  mkdir $prefix, 0700 unless -d $prefix;
+
+  open(NAMED_CONF,">$prefix/named.conf")
+    or die "can't open $prefix/named.conf: $!";
+
+  open(CONF_HEADER,"<$prefix/named.conf.HEADER")
+    or die "can't open $prefix/named.conf.HEADER: $!";
+  while (<CONF_HEADER>) { print NAMED_CONF $_; }
+  close CONF_HEADER;
+
+  my $zonepath = $export->option('zonepath');
+  $zonepath =~ s/\/$//;
+
+  my @svc_domain = $export->svc_x;
+
+  foreach my $svc_domain ( @svc_domain ) {
+    my $domain = $svc_domain->domain;
+    my @masters = qsearch('domain_record', {
+      'svcnum' => $svc_domain->svcnum,
+      'rectype' => '_mstr',
+    } );
+    if ( @masters ) {
+      my $masters = join('; ', map { $_->recdata } @masters );
+
+      print NAMED_CONF <<END;
+zone "$domain" {
+       type slave;
+       file "db.$domain";
+       masters { $masters; };
+};
+
+END
+
+    } else {
+
+      print NAMED_CONF <<END;
+zone "$domain" {
+       type master;
+       file "$zonepath/db.$domain";
+};
+
+END
+
+      open (DB_MASTER,">$prefix/db.$domain")
+        or die "can't open $prefix/db.$domain: $!";
+
+      if ($bind_rel eq 'BIND9') {
+        print DB_MASTER "\$TTL $minttl\n\$ORIGIN $domain.\n";
+      }
+
+      my @domain_records =
+        qsearch('domain_record', { 'svcnum' => $svc_domain->svcnum } );
+      foreach my $domain_record (
+        sort { $b->rectype cmp $a->rectype } @domain_records
+      ) {
+        #if ( $domain_record->rectype eq 'SOA' ) {
+        #  print DB_MASTER join("\t", $domain_record-> reczone
+        #} else {
+          print DB_MASTER join("\t",
+            map { $domain_record->getfield($_) }
+              qw( reczone recaf rectype recdata )
+          ), "\n";
+        #}
+      }
+
+      close DB_MASTER;
+
+    }
+
+  }
+
+  $rsync->exec( {
+    src       => "$prefix/",
+    recursive => 1,
+    dest      => "root\@$machine:$zonepath/",
+    exclude   => [qw( *.import named.conf.HEADER named.conf )],
+  } ) or die "rsync to $machine failed: ". join(" / ", $rsync->err);
+ # warn $rsync->out;
+
+  $rsync->exec( {
+    src     => "$prefix/named.conf",
+    dest    => "root\@$machine:". $export->option('named_conf'),
+  } ) or die "rsync to $machine failed: ". join(" / ", $rsync->err);
+#  warn $rsync->out;
+
+  ssh("root\@$machine", "$ndc_cmd reload");
+
+}
+
+close NAMED_CONF;
+
+foreach my $sexport ( @sexports ) { #false laziness with above
+
+  my $machine = $sexport->machine;
+  my $prefix = "$spooldir/$machine";
+
+  my $bind_rel = $sexport->option('bind_release');
+  my $ndc_cmd = ($bind_rel eq 'BIND9') ? 'rndc' : 'ndc';
+
+  #prevent old domain files from piling up
+  #rmtree "$prefix" or die "can't rmtree $prefix.db: $!";
+
+  mkdir $prefix, 0700 unless -d $prefix;
+
+  open(NAMED_CONF,">$prefix/named.conf")
+    or die "can't open $prefix/named.conf: $!";
+
+  open(CONF_HEADER,"<$prefix/named.conf.HEADER")
+    or die "can't open $prefix/named.conf.HEADER: $!";
+  while (<CONF_HEADER>) { print NAMED_CONF $_; }
+  close CONF_HEADER;
+
+  my $masters = $sexport->option('master');
+
+  #false laziness with  freeside-sqlradius-reset 
+  my @svc_domain =
+    map { qsearchs('svc_domain', { 'svcnum' => $_->svcnum } ) }
+      map { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
+        grep { qsearch('cust_svc', { 'svcpart' => $_->svcpart } ) }
+          $sexport->export_svc;
+
+  foreach my $svc_domain ( @svc_domain ) {
+    my $domain = $svc_domain->domain;
+    print NAMED_CONF <<END;
+zone "$domain" {
+       type slave;
+       file "db.$domain";
+       masters { $masters; };
+};
+
+END
+
+  }
+
+  $rsync->exec( {
+    src     => "$prefix/named.conf",
+    dest    => "root\@$machine:". $sexport->option('named_conf'),
+  } ) or die "rsync to $machine failed: ". join(" / ", $rsync->err);
+#  warn $rsync->out;
+
+  ssh("root\@$machine", "$ndc_cmd reload");
+
+}
+close NAMED_CONF;
+
+# -----
+
+sub usage {
+  die "Usage:\n  bind.export user\n"; 
+}
+
diff --git a/bin/bind.import b/bin/bind.import
new file mode 100755 (executable)
index 0000000..57eca2b
--- /dev/null
@@ -0,0 +1,192 @@
+#!/usr/bin/perl -w
+#
+# $Id: bind.import,v 1.3 2002-07-15 01:44:23 ivan Exp $
+
+#need to manually put header in /usr/local/etc/freeside/export.<datasrc./bind/<machine>/named.conf.HEADER
+
+use strict;
+use vars qw( %d_part_svc );
+use Term::Query qw(query);
+#use BIND::Conf_Parser;
+#use DNS::ZoneParse 0.81;
+
+#use Net::SCP qw(iscp);
+use Net::SCP qw(scp);
+use FS::UID qw(adminsuidsetup datasrc);
+use FS::Record qw(qsearch); #qsearchs);
+#use FS::svc_acct_sm;
+use FS::svc_domain;
+use FS::domain_record;
+#use FS::svc_acct;
+#use FS::part_svc;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+$FS::svc_Common::noexport_hack = 1;
+$FS::domain_record::noserial_hack = 1;
+
+use vars qw($spooldir);
+$spooldir = "/usr/local/etc/freeside/export.". datasrc. "/bind";
+mkdir $spooldir unless -d $spooldir;
+
+%d_part_svc =
+  map { $_->svcpart, $_ } qsearch('part_svc',{'svcdb'=>'svc_domain'});
+
+print "\n\n",
+      ( join "\n", map "$_: ".$d_part_svc{$_}->svc, sort keys %d_part_svc ),
+      "\n\n";
+use vars qw($domain_svcpart);
+$^W=0; #Term::Query isn't -w-safe
+$domain_svcpart =
+  query "Enter part number for domains: ", 'irk', [ keys %d_part_svc ];
+$^W=1;
+
+print "\n\n", <<END;
+Enter the location and name of your primary named.conf file, for example
+"ns.isp.com:/var/named/named.conf"
+END
+my($named_conf)=&getvalue(":");
+  
+use vars qw($named_machine $prefix);
+$named_machine = (split(/:/, $named_conf))[0];
+$prefix = "$spooldir/$named_machine";
+mkdir $prefix unless -d $prefix;
+
+#iscp("root\@$named_conf","$prefix/named.conf.import");
+scp("root\@$named_conf","$prefix/named.conf.import");
+
+
+sub getvalue {
+  my $prompt = shift;
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query $prompt, '';
+  $^W=1;
+  $return;
+}
+
+print "\n\n";
+
+##
+
+$FS::svc_domain::whois_hack=1;
+
+my $p = Parser->new;
+$p->parse_file("$prefix/named.conf.import");
+
+print "\nBIND import completed.\n";
+
+##
+
+sub usage {
+  die "Usage:\n\n  svc_domain.import user\n";
+}
+
+########
+BEGIN {
+  
+  package Parser;
+  use BIND::Conf_Parser;
+  use vars qw(@ISA $named_dir);
+  @ISA = qw(BIND::Conf_Parser);
+  
+  sub handle_option {
+    my($self, $option, $argument) = @_;
+    return unless $option eq "directory";
+    $named_dir = $argument;
+  }
+  
+  sub handle_zone {
+    my($self, $name, $class, $type, $options) = @_;
+    return unless $class eq 'in';
+    return if grep { $name eq $_ }
+      ( qw( . localhost 127.in-addr.arpa 0.in-addr.arpa 255.in-addr.arpa ) );
+
+    my $domain = new FS::svc_domain( {
+      svcpart => $main::domain_svcpart,
+      domain  => $name,
+      action  => 'N',
+    } );
+    my $error = $domain->insert;
+    die $error if $error;
+
+    if ( $type eq 'slave' ) {
+
+      #use Data::Dumper;
+      #print Dumper($options);
+      #exit;
+
+      foreach my $master ( @{ $options->{masters} } ) {
+        my $domain_record = new FS::domain_record( {
+          'svcnum'  => $domain->svcnum,
+          'reczone' => '@',
+          'recaf'   => 'IN',
+          'rectype' => '_mstr',
+          'recdata' => $master,
+        } );
+        my $error = $domain_record->insert;
+        die $error if $error;
+      }
+
+    } elsif ( $type eq 'master' ) {
+
+      my $file = $options->{file};
+  
+      use File::Basename;
+      my $basefile = basename($file);
+      my $sourcefile = $file;
+      $sourcefile = "$named_dir/$sourcefile" unless $file =~ /^\//;
+      use Net::SCP qw(iscp scp);
+      scp("root\@$main::named_machine:$sourcefile",
+          "$main::prefix/$basefile.import");
+    
+      use DNS::ZoneParse 0.81;
+      my $zone = DNS::ZoneParse->new("$main::prefix/$basefile.import");
+    
+      my $dump = $zone->Dump;
+  
+      #use Data::Dumper;
+      #print "$name: ". Dumper($dump);
+      #exit;
+    
+      foreach my $rectype ( keys %$dump ) {
+        if ( $rectype =~ /^SOA$/i ) {
+          my $rec = $dump->{$rectype};
+          my $domain_record = new FS::domain_record( {
+            'svcnum'  => $domain->svcnum,
+            'reczone' => $rec->{origin},
+            'recaf'   => 'IN',
+            'rectype' => $rectype,
+            'recdata' =>
+              $rec->{primary}. ' '. $rec->{email}. ' ( '.
+             join(' ', map $rec->{$_},
+                           qw( serial refresh retry expire minimumTTL ) ).
+             ' )',
+          } );
+          my $error = $domain_record->insert;
+          die $error if $error;
+       } else {
+          #die $dump->{$rectype};
+          foreach my $rec ( @{ $dump->{$rectype} } ) {
+            my $domain_record = new FS::domain_record( {
+              'svcnum'  => $domain->svcnum,
+              'reczone' => $rec->{name},
+              'recaf'   => $rec->{class},
+              'rectype' => $rectype,
+              'recdata' => ( $rectype =~ /^MX$/i
+                               ? $rec->{priority}. ' '. $rec->{host}
+                               : $rec->{host}                      ),
+            } );
+            my $error = $domain_record->insert;
+            die $error if $error;
+          }
+        }
+      }
+
+    }
+    
+  }
+
+}
+#########
+
diff --git a/bin/bsdshell.export b/bin/bsdshell.export
new file mode 100755 (executable)
index 0000000..6e0d103
--- /dev/null
@@ -0,0 +1,114 @@
+#!/usr/bin/perl -w
+
+# bsdshell export
+
+use strict;
+use File::Rsync;
+use Net::SSH qw(ssh);
+use FS::UID qw(adminsuidsetup datasrc);
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export;
+use FS::cust_svc;
+use FS::svc_acct;
+
+my @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $spooldir = "/usr/local/etc/freeside/export.". datasrc;
+#my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/shell";
+
+my @bsd_exports = qsearch('part_export', { 'exporttype' => 'bsdshell' } );
+
+my $rsync = File::Rsync->new({
+  rsh     => 'ssh',
+#  dry_run => 1,
+});
+
+foreach my $export ( @bsd_exports ) {
+  my $machine = $export->machine;
+  my $prefix = "$spooldir/$machine";
+  mkdir $prefix, 0700 unless -d $prefix;
+
+  #LOCKING!!!
+
+  ( open(MASTER,">$prefix/master.passwd")
+    #!!!  and flock(MASTER,LOCK_EX|LOCK_NB)
+  ) or die "Can't open $prefix/master.passwd: $!";
+  ( open(PASSWD,">$prefix/passwd")
+    #!!!  and flock(PASSWD,LOCK_EX|LOCK_NB)
+  ) or die "Can't open $prefix/passwd: $!";
+
+  chmod 0644, "$prefix/passwd";
+  chmod 0600, "$prefix/master.passwd";
+
+  my @svc_acct = $export->svc_x;
+
+  next unless @svc_acct;
+
+  foreach my $svc_acct ( sort { $a->uid <=> $b->uid } @svc_acct ) {
+
+    my $password = $svc_acct->_password;
+    my $cpassword;
+    #if ( ( length($password) <= 8 )
+    if ( ( length($password) <= 12 )
+         && ( $password ne '*' )
+         && ( $password ne '!!' )
+         && ( $password ne '' )
+    ) {
+      $cpassword=crypt($password,
+                       $saltset[int(rand(64))].$saltset[int(rand(64))]
+      );
+      # MD5 !!!!
+    } else {
+      $cpassword=$password;
+    }
+
+    ###
+    # FORMAT OF THE PASSWD FILE HERE
+    print PASSWD join(":",
+      $svc_acct->username,
+      'x', # "##". $username,
+      $svc_acct->uid,
+      $svc_acct->gid,
+      $svc_acct->finger,
+      $svc_acct->dir,
+      $svc_acct->shell,
+    ), "\n";
+
+    ###
+    # FORMAT OF FreeBSD MASTER PASSWD FILE HERE
+    print MASTER join(":",
+      $svc_acct->username,              # User name
+      $cpassword,                       # Encrypted password
+      $svc_acct->uid,                   # User ID
+      $svc_acct->gid,                   # Group ID
+      "",                               # Login Class
+      "0",                              # Password Change Time
+      "0",                              # Password Expiration Time
+      $svc_acct->finger,                # Users name
+      $svc_acct->dir,                   # Users home directory
+      $svc_acct->shell,                 # shell
+    ), "\n" ;
+  
+  }
+
+  #!!! flock(MASTER,LOCK_UN);
+  #!!! flock(PASSWD,LOCK_UN);
+  close MASTER;
+  close PASSWD;
+
+  $rsync->exec( {
+    src  => "$prefix/passwd",
+    dest => "root\@$machine:/etc/passwd"
+  } ) or die "rsync to $machine failed: ". join(" / ", $rsync->err);
+
+  $rsync->exec( {
+    src  => "$prefix/master.passwd",
+    dest => "root\@$machine:/etc/master.passwd.new"
+  } ) or die "rsync to $machine failed: ". join(" / ", $rsync->err);
+  ssh("root\@$machine", "pwd_mkdb /etc/master.passwd.new");
+
+  # UNLOCK!!
+}
diff --git a/bin/create-history-tables b/bin/create-history-tables
new file mode 100755 (executable)
index 0000000..39248bf
--- /dev/null
@@ -0,0 +1,93 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use DBI;
+use DBIx::DBSchema 0.21;
+use DBIx::DBSchema::Table;
+use DBIx::DBSchema::Column;
+use DBIx::DBSchema::ColGroup::Unique;
+use DBIx::DBSchema::ColGroup::Index;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(dbdef);
+
+my $user = shift or die &usage;
+my $dbh = adminsuidsetup $user;
+
+my $schema = dbdef();
+
+#false laziness w/fs-setup
+my @tables = scalar(@ARGV)
+               ? @ARGV
+               : grep { ! /^(h|pg)_/ } $schema->tables;
+foreach my $table ( @tables ) {
+  next if grep { /^h_$table/ } $schema->tables;
+  warn "creating history table for $table\n";
+  my $tableobj = $schema->table($table)
+    or die "unknown table $table (did you run dbdef-create?)\n";
+  my $h_tableobj = DBIx::DBSchema::Table->new( {
+    name        => "h_$table",
+    primary_key => 'historynum',
+    unique      => DBIx::DBSchema::ColGroup::Unique->new( [] ),
+    'index'     => DBIx::DBSchema::ColGroup::Index->new( [
+                     @{$tableobj->unique->lol_ref},
+                     @{$tableobj->index->lol_ref}
+                   ] ),
+    columns     => [
+                     DBIx::DBSchema::Column->new( {
+                       'name'    => 'historynum',
+                       'type'    => 'serial',
+                       'null'    => 'NOT NULL',
+                       'length'  => '',
+                       'default' => '',
+                       'local'   => '',
+                     } ),
+                     DBIx::DBSchema::Column->new( {
+                       'name'    => 'history_date',
+                       'type'    => 'int',
+                       'null'    => 'NULL',
+                       'length'  => '',
+                       'default' => '',
+                       'local'   => '',
+                     } ),
+                     DBIx::DBSchema::Column->new( {
+                       'name'    => 'history_user',
+                       'type'    => 'varchar',
+                       'null'    => 'NOT NULL',
+                       'length'  => '80',
+                       'default' => '',
+                       'local'   => '',
+                     } ),
+                     DBIx::DBSchema::Column->new( {
+                       'name'    => 'history_action',
+                       'type'    => 'varchar',
+                       'null'    => 'NOT NULL',
+                       'length'  => '80',
+                       'default' => '',
+                       'local'   => '',
+                     } ),
+                     map {
+                       my $column = $tableobj->column($_);
+                       $column->type('int')
+                         if $column->type eq 'serial';
+                       $column->default('')
+                         if $column->default =~ /^nextval\(/i;
+                       ( my $local = $column->local ) =~ s/AUTO_INCREMENT//i;
+                       $column->local($local);
+                       $column;
+                     } $tableobj->columns
+                   ],
+  } );
+  foreach my $statement ( $h_tableobj->sql_create_table($dbh) ) {
+    $dbh->do( $statement )
+      or die "CREATE error: ". $dbh->errstr. "\ndoing statement: $statement";
+  }
+
+}
+
+$dbh->commit or die $dbh->errstr;
+$dbh->disconnect or die $dbh->errstr;
+
+sub usage {
+  die "Usage:\n  create-history-tables user [ table table ... ] \n";
+}
+
index eb62c77..c977f87 100755 (executable)
@@ -1,85 +1,26 @@
 #!/usr/bin/perl -Tw
 #
-# create dbdef file for existing mySQL database (needs SHOW|DESCRIBE command
-# not in Pg) based on fs-setup
-#
-# ivan@sisd.com 98-jun-2
+# $Id: dbdef-create,v 1.6 2002-09-19 13:34:52 ivan Exp $
 
 use strict;
 use DBI;
-use FS::dbdef;
-use FS::UID qw(adminsuidsetup datasrc);
-
-#needs to match FS::Record
-my($dbdef_file) = "/var/spool/freeside/dbdef.". datasrc;
-
-my($dbh)=adminsuidsetup;
-
-my($tables_sth)=$dbh->prepare("SHOW TABLES");
-my($tables_rv)=$tables_sth->execute;
+use DBIx::DBSchema 0.21;
+use FS::UID qw(adminsuidsetup datasrc driver_name);
 
-my(@tables);
-foreach ( @{$tables_sth->fetchall_arrayref} ) {
-  my($table)=${$_}[0]; 
-  #print "TABLE\t$table\n";
+my $user = shift or die &usage;
 
-  my($index_sth)=$dbh->prepare("SHOW INDEX FROM $table");
-  my($primary_key)='';
-  my(%index,%unique);
-  for ( 1 .. $index_sth->execute ) {
-    my($row)=$index_sth->fetchrow_hashref;
-    if ( ${$row}{'Key_name'} eq "PRIMARY" ) {
-      $primary_key=${$row}{'Column_name'};
-      next;
-    }
-    if ( ${$row}{'Non_unique'} ) { #index
-      push @{$index{${$row}{'Key_name'}}}, ${$row}{'Column_name'};
-    } else { #unique
-      push @{$unique{${$row}{'Key_name'}}}, ${$row}{'Column_name'};
-    }
-  }
+my($dbh)=adminsuidsetup $user;
 
-  my(@index)=values %index;
-  my(@unique)=values %unique;
-  #print "\tPRIMARY KEY $primary_key\n";
-  foreach (@index) {
-    #print "\tINDEX\t", join(', ', @{$_}), "\n";
-  }
-  foreach (@unique) {
-    #print "\tUNIQUE\t", join(', ', @{$_}), "\n";
-  }
-
-  my($columns_sth)=$dbh->prepare("SHOW COLUMNS FROM $table");
-  my(@columns);
-  for ( 1 .. $columns_sth->execute ) {
-    my($row)=$columns_sth->fetchrow_hashref;
-    #print "\t", ${$row}{'Field'}, "\n";
-    ${$row}{'Type'} =~ /^(\w+)\(?([\d\,]+)?\)?( unsigned)?$/
-      or die "Illegal type ${$row}{'Type'}\n";
-    my($type,$length)=($1,$2);
-    my($null)=${$row}{'Null'};
-    $null =~ s/YES/NULL/;
-    push @columns, new FS::dbdef_column (
-      ${$row}{'Field'},
-      $type,
-      $null,
-      $length,
-    );
-  }
+#needs to match FS::Record
+my($dbdef_file) = "/usr/local/etc/freeside/dbdef.". datasrc;
 
-  #print "\n";
-  push @tables, new FS::dbdef_table (
-    $table,
-    $primary_key,
-    new FS::dbdef_unique (\@unique),
-    new FS::dbdef_index (\@index),
-    @columns,
-  );
+my $dbdef = new_native DBIx::DBSchema $dbh;
 
-}
-
-my($dbdef) = new FS::dbdef ( @tables );
+#print $dbdef->pretty_print;
 
 #important
 $dbdef->save($dbdef_file);
 
+sub usage {
+  die "Usage:\n  dbdef-create user\n";
+}
diff --git a/bin/fix-sequences b/bin/fix-sequences
new file mode 100755 (executable)
index 0000000..2ff89d3
--- /dev/null
@@ -0,0 +1,69 @@
+#!/usr/bin/perl -Tw
+
+# run dbdef-create first!
+
+use strict;
+use DBI;
+use DBIx::DBSchema 0.21;
+use DBIx::DBSchema::Table;
+use DBIx::DBSchema::Column;
+use DBIx::DBSchema::ColGroup::Unique;
+use DBIx::DBSchema::ColGroup::Index;
+use FS::UID qw(adminsuidsetup driver_name);
+use FS::Record qw(dbdef);
+
+my $user = shift or die &usage;
+my $dbh = adminsuidsetup $user;
+
+my $schema = dbdef();
+
+#false laziness w/fs-setup
+my @tables = scalar(@ARGV)
+               ? @ARGV
+               : grep { ! /^h_/ } $schema->tables;
+foreach my $table ( @tables ) {
+  my $tableobj = $schema->table($table)
+    or die "unknown table $table (did you run dbdef-create?)\n";
+
+  my $primary_key = $tableobj->primary_key;
+  next unless $primary_key;
+
+  my $col = $tableobj->column($primary_key);
+
+
+  next unless uc($col->type) eq 'SERIAL'
+              || ( driver_name eq 'Pg'
+                     && defined($col->default)
+                     && $col->default =~ /^nextval\(/i
+                 )
+              || ( driver_name eq 'mysql'
+                     && defined($col->local)
+                     && $col->local =~ /AUTO_INCREMENT/i
+                 );
+
+  my $seq = "${table}_${primary_key}_seq";
+  if ( driver_name eq 'Pg'
+       && defined($col->default) 
+       && $col->default =~ /^nextval\('"(public\.)?(\w+_seq)"'::text\)$/
+     ) {
+    $seq = $2;
+  }
+
+  warn "fixing sequence for $table\n";
+
+
+  my $sql = "SELECT setval( '$seq',
+                            ( SELECT max($primary_key) FROM $table ) );";
+
+  #warn $col->default. " $seq\n$sql\n";
+  $dbh->do( $sql ) or die $dbh->errstr;
+
+}
+
+$dbh->commit or die $dbh->errstr;
+$dbh->disconnect or die $dbh->errstr;
+
+sub usage {
+  die "Usage:\n  fix-sequences user [ table table ... ] \n";
+}
+
diff --git a/bin/freeside-init b/bin/freeside-init
new file mode 100755 (executable)
index 0000000..fe12931
--- /dev/null
@@ -0,0 +1,60 @@
+#! /bin/sh
+#
+# start the freeside job queue daemon
+
+#PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+DAEMON=/usr/local/bin/freeside-queued
+NAME=freeside-queued
+DESC="freeside job queue daemon"
+USER="ivan"
+
+test -f $DAEMON || exit 0
+
+set -e
+
+case "$1" in
+  start)
+       echo -n "Starting $DESC: "
+#      start-stop-daemon --start --quiet --pidfile /var/run/$NAME.pid -b -m\
+#              --exec $DAEMON
+       $DAEMON $USER &
+       echo "$NAME."
+       ;;
+  stop)
+       echo -n "Stopping $DESC: "
+       start-stop-daemon --stop --quiet --pidfile /var/run/$NAME.pid \
+               --exec $DAEMON
+       echo "$NAME."
+        rm /var/run/$NAME.pid
+       ;;
+  #reload)
+       #
+       #       If the daemon can reload its config files on the fly
+       #       for example by sending it SIGHUP, do it here.
+       #
+       #       If the daemon responds to changes in its config file
+       #       directly anyway, make this a do-nothing entry.
+       #
+       # echo "Reloading $DESC configuration files."
+       # start-stop-daemon --stop --signal 1 --quiet --pidfile \
+       #       /var/run/$NAME.pid --exec $DAEMON
+  #;;
+  restart|force-reload)
+       #
+       #       If the "reload" option is implemented, move the "force-reload"
+       #       option to the "reload" entry above. If not, "force-reload" is
+       #       just the same as "restart".
+       #
+        $0 stop
+       sleep 1
+        $0 start
+       ;;
+  *)
+       N=/etc/init.d/$NAME
+       # echo "Usage: $N {start|stop|restart|reload|force-reload}" >&2
+       echo "Usage: $N {start|stop|restart|force-reload}" >&2
+       exit 1
+       ;;
+esac
+
+exit 0
diff --git a/bin/freeside-session-kill b/bin/freeside-session-kill
new file mode 100755 (executable)
index 0000000..d5fd703
--- /dev/null
@@ -0,0 +1,103 @@
+#!/usr/bin/perl -w
+
+use strict;
+use vars qw($conf);
+use Fcntl qw(:flock);
+use FS::UID qw(adminsuidsetup datasrc dbh);
+use FS::Record qw(dbdef qsearch fields);
+use FS::session;
+use FS::svc_acct;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $sessionlock = "/usr/local/etc/freeside/session-kill.lock.". datasrc;
+
+open(LOCK,"+>>$sessionlock") or die "Can't open $sessionlock: $!";
+select(LOCK); $|=1; select(STDOUT);
+unless ( flock(LOCK,LOCK_EX|LOCK_NB) ) {
+  seek(LOCK,0,0);
+  my($pid)=<LOCK>;
+  chop($pid);
+  #no reason to start loct of blocking processes
+  die "Is another session kill process running under pid $pid?\n";
+}
+seek(LOCK,0,0);
+print LOCK $$,"\n";
+
+$FS::UID::AutoCommit = 0;
+
+my $now = time;
+
+#uhhhhh
+
+use DBIx::DBSchema;
+use DBIx::DBSchema::Table; #down this path lies madness
+use DBIx::DBSchema::Column;
+
+my $dbdef = dbdef or die;
+#warn $dbdef;
+#warn $dbdef->{'tables'};
+#warn keys %{$dbdef->{'tables'}};
+my $session_table = $dbdef->table('session') or die;
+my $svc_acct_table = $dbdef->table('svc_acct') or die;
+
+my $session_svc_acct = new DBIx::DBSchema::Table ( 'session,svc_acct', '', '', '',
+  map( DBIx::DBSchema::Column->new( "session.$_",
+                              $session_table->column($_)->type,
+                              $session_table->column($_)->null,
+                              $session_table->column($_)->length,
+  ), $session_table->columns() ),
+  map( DBIx::DBSchema::Column->new( "svc_acct.$_",
+                              $svc_acct_table->column($_)->type,
+                              $svc_acct_table->column($_)->null,
+                              $svc_acct_table->column($_)->length,
+  ), $svc_acct_table->columns ),
+#  map("svc_acct.$_", $svc_acct_table->columns),
+);
+
+$dbdef->addtable($session_svc_acct); #madness, i tell you
+
+$FS::Record::DEBUG = 1;
+my @session = qsearch('session,svc_acct', {}, '', ' WHERE '. join(' AND ',
+  'svc_acct.svcnum = session.svcnum',
+  '( session.logout IS NULL OR session.logout = 0 )',
+  "( $now - session.login ) >= svc_acct.seconds"
+). " FOR UPDATE" );
+
+my $dbh = dbh;
+
+foreach my $join ( @session ) {
+
+  my $session = new FS::session ( {
+    map { $_ => $join->{'Hash'}{"session.$_"} } fields('session')
+  } ); #see no evil
+
+  my $svc_acct = new FS::svc_acct ( {
+    map { $_ => $join->{'Hash'}{"svc_acct.$_"} } fields('svc_acct')
+  } );
+
+  #false laziness w/ fs_session_server
+  my $nsession = new FS::session ( { $session->hash } );
+  my $error = $nsession->replace($session);
+  if ( $error ) {
+    $dbh->rollback;
+    die $error;
+  }
+  my $time = $nsession->logout - $nsession->login;
+  my $new_svc_acct = new FS::svc_acct ( { $svc_acct->hash } );
+  my $seconds = $new_svc_acct->seconds;
+  $seconds -= $time;
+  $seconds = 0 if $seconds < 0;
+  $new_svc_acct->seconds( $seconds );
+  $error = $new_svc_acct->replace( $svc_acct );
+  warn "can't debit time from ". $svc_acct->username. ": $error\n"; #don't want to rollback, though
+  #ssenizal eslaf
+
+}
+
+$dbh->commit or die $dbh->errstr;
+
+sub usage {
+  die "Usage:\n\n  freeside-session-kill user\n";
+}
diff --git a/bin/fs-migrate-part_svc b/bin/fs-migrate-part_svc
new file mode 100755 (executable)
index 0000000..b0f3ac5
--- /dev/null
@@ -0,0 +1,41 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch fields);
+use FS::part_svc;
+
+my $user = shift or die &usage;
+my $dbh = adminsuidsetup $user;
+
+my $oldAutoCommit = $FS::UID::AutoCommit;
+local $FS::UID::AutoCommit = 0;
+
+foreach my $part_svc ( qsearch('part_svc', {} ) ) {
+  foreach my $field (
+    grep { defined($part_svc->getfield($part_svc->svcdb.'__'.$_.'_flag') ) }
+      fields($part_svc->svcdb)
+  ) {
+    my $flag = $part_svc->getfield($part_svc->svcdb.'__'.$field.'_flag');
+    if ( uc($flag) =~ /^([DF])$/ ) {
+      my $part_svc_column = new FS::part_svc_column {
+        'svcpart' => $part_svc->svcpart,
+        'columnname' => $field,
+        'columnflag' => $1,
+        'columnvalue' => $part_svc->getfield($part_svc->svcdb.'__'.$field),
+      };
+      my $error = $part_svc_column->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        die $error;
+      }
+    }
+  }
+}
+
+$dbh->commit or die $dbh->errstr;
+
+sub usage {
+  die "Usage:\n  fs-migrate-part_svc user\n"; 
+}
+
diff --git a/bin/fs-migrate-payref b/bin/fs-migrate-payref
new file mode 100755 (executable)
index 0000000..1584197
--- /dev/null
@@ -0,0 +1,31 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::cust_pay;
+use FS::cust_refund;
+
+my $user = shift or die &usage;
+my $dbh = adminsuidsetup $user;
+
+# apply payments to invoices
+
+foreach my $cust_pay ( qsearch('cust_pay', {} ) ) {
+  my $error = $cust_pay->upgrade_replace;
+  warn $error if $error;
+}
+
+# apply refunds to credits
+
+foreach my $cust_refund ( qsearch('cust_refund') ) {
+  my $error = $cust_refund->upgrade_replace;
+  warn $error if $error;
+}
+
+# ? apply credits to invoices
+
+sub usage {
+  die "Usage:\n  fs-migrate-payref user\n"; 
+}
+
diff --git a/bin/fs-migrate-svc_acct_sm b/bin/fs-migrate-svc_acct_sm
new file mode 100755 (executable)
index 0000000..e34b235
--- /dev/null
@@ -0,0 +1,229 @@
+#!/usr/bin/perl -Tw
+#
+# $Id: fs-migrate-svc_acct_sm,v 1.4 2002-06-21 09:13:16 ivan Exp $
+#
+# jeff@cmh.net 01-Jul-20
+
+#to delay loading dbdef until we're ready
+#BEGIN { $FS::Record::setup_hack = 1; }
+
+use strict;
+use Term::Query qw(query);
+#use DBI;
+#use DBIx::DBSchema;
+#use DBIx::DBSchema::Table;
+#use DBIx::DBSchema::Column;
+#use DBIx::DBSchema::ColGroup::Unique;
+#use DBIx::DBSchema::ColGroup::Index;
+use FS::Conf;
+use FS::UID qw(adminsuidsetup datasrc checkeuid getsecrets);
+use FS::Record qw(qsearch qsearchs);
+use FS::svc_domain;
+use FS::svc_forward;
+use vars qw( $conf $old_default_domain %part_domain_svc %part_acct_svc %part_forward_svc $svc_acct $svc_acct_sm $error);
+
+die "Not running uid freeside!" unless checkeuid();
+
+my $user = shift or die &usage;
+getsecrets($user);
+
+$conf = new FS::Conf;
+$old_default_domain = $conf->config('domain');
+
+#needs to match FS::Record
+#my($dbdef_file) = "/usr/local/etc/freeside/dbdef.". datasrc;
+
+###
+# This section would be the appropriate place to manipulate
+# the schema & tables.
+###
+
+##  we need to add the domsvc to svc_acct
+##  we must add a svc_forward record....
+##  I am thinking that the fields  svcnum (int), destsvc (int), and
+##  dest (varchar (80))  are appropriate, with destsvc/dest an either/or
+##  much in the spirit of cust_main_invoice
+
+###
+# massage the data
+###
+
+my($dbh)=adminsuidsetup $user;
+
+$|=1;
+
+$FS::svc_Common::noexport_hack = 1;
+$FS::svc_domain::whois_hack = 1;
+
+%part_domain_svc=map { $_->svcpart, $_ } qsearch('part_svc',{'svcdb'=>'svc_domain'});
+%part_acct_svc=map { $_->svcpart, $_ } qsearch('part_svc',{'svcdb'=>'svc_acct'});
+%part_forward_svc=map { $_->svcpart, $_ } qsearch('part_svc',{'svcdb'=>'svc_forward'});
+
+die "No services with svcdb svc_domain!\n" unless %part_domain_svc;
+die "No services with svcdb svc_acct!\n" unless %part_acct_svc;
+die "No services with svcdb svc_forward!\n" unless %part_forward_svc;
+
+my($svc_domain) = qsearchs('svc_domain', { 'domain' => $old_default_domain });
+if (! $svc_domain || $svc_domain->domain != $old_default_domain) {
+   print <<EOF;
+
+Your database currently does not contain a svc_domain record for the
+domain $old_default_domain.  Would you like me to add one for you?
+EOF
+
+   my($response)=scalar(<STDIN>);
+   chop $response;
+   if ($response =~ /^[yY]/) {
+      print "\n\n", &menu_domain_svc, "\n", <<END;
+I need to create new domain accounts.  Which service shall I use for that?
+END
+      my($domain_svcpart)=&getdomainpart;
+
+      $svc_domain = new FS::svc_domain {
+        'domain' => $old_default_domain,
+        'svcpart' => $domain_svcpart,
+        'action' => 'M',
+       };
+#      $error=$svc_domain->insert && die "Error adding domain $old_default_domain: $error";
+      $error=$svc_domain->insert;
+      die "Error adding domain $old_default_domain: $error" if $error;
+   }else{
+      print <<EOF;
+
+  This program cannot function properly until a svc_domain record matching
+your conf_dir/domain file exists.
+EOF
+
+      exit 1;
+   }
+}
+
+print "\n\n", &menu_acct_svc, "\n", <<END;
+I may need to create some new pop accounts and set up forwarding to them
+for some users.  Which service shall I use for that?
+END
+my($pop_svcpart)=&getacctpart;
+
+print "\n\n", &menu_forward_svc, "\n", <<END;
+I may need to create some new forwarding for some users.  Which service
+shall I use for that?
+END
+my($forward_svcpart)=&getforwardpart;
+
+sub menu_domain_svc {
+  ( join "\n", map "$_: ".$part_domain_svc{$_}->svc, sort keys %part_domain_svc ). "\n";
+}
+sub menu_acct_svc {
+  ( join "\n", map "$_: ".$part_acct_svc{$_}->svc, sort keys %part_acct_svc ). "\n";
+}
+sub menu_forward_svc {
+  ( join "\n", map "$_: ".$part_forward_svc{$_}->svc, sort keys %part_forward_svc ). "\n";
+}
+sub getdomainpart {
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query "Enter part number:", 'irk', [ keys %part_domain_svc ];
+  $^W=1;
+  $return;
+}
+sub getacctpart {
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query "Enter part number:", 'irk', [ keys %part_acct_svc ];
+  $^W=1;
+  $return;
+}
+sub getforwardpart {
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query "Enter part number:", 'irk', [ keys %part_forward_svc ];
+  $^W=1;
+  $return;
+}
+
+
+#migrate data
+
+my(@svc_accts) = qsearch('svc_acct', {});
+foreach $svc_acct (@svc_accts) {
+  my(@svc_acct_sms) = qsearch('svc_acct_sm', {
+      domuid => $svc_acct->getfield('uid'),
+      }
+    );
+
+  #  Ok.. we've got the svc_acct record, and an array of svc_acct_sm's
+  #  What do we do from here?
+
+  #  The intuitive:
+  #    plop the svc_acct into the 'default domain'
+  #    and then represent the svc_acct_sm's with svc_forwards
+  #    they can be gussied up manually, later
+  #
+  #  Perhaps better:
+  #    when no svc_acct_sm exists, place svc_acct in 'default domain'
+  #    when one svc_acct_sm exists, place svc_acct in corresponding
+  #      domain & possibly create a svc_forward in 'default domain'
+  #    when multiple svc_acct_sm's exists (in different domains) we'd
+  #    better use the 'intuitive' approach.
+  #
+  #  Specific way:
+  #    as 'perhaps better,' but we may be able to guess which domain
+  #    is correct by comparing the svcnum of domains to the username
+  #    of the svc_acct
+  #
+
+  # The intuitive way:
+
+  my $def_acct = new FS::svc_acct ( { $svc_acct->hash } );
+  $def_acct->setfield('domsvc' => $svc_domain->getfield('svcnum'));
+  $error = $def_acct->replace($svc_acct);
+  die "Error replacing svc_acct for " . $def_acct->username . " : $error" if $error;
+
+  foreach $svc_acct_sm (@svc_acct_sms) {
+
+    my($domrec)=qsearchs('svc_domain', {
+      svcnum => $svc_acct_sm->getfield('domsvc'),
+    }) || die  "svc_acct_sm references invalid domsvc $svc_acct_sm->getfield('domsvc')\n";
+
+    if ($svc_acct_sm->getfield('domuser') =~ /^\*$/) {
+      
+      my($newdom) = new FS::svc_domain ( { $domrec->hash } );
+      $newdom->setfield('catchall', $svc_acct->svcnum);
+      $newdom->setfield('action', "M");
+      $error = $newdom->replace($domrec);
+      die "Error replacing svc_domain for (anything)@" . $domrec->domain . " : $error" if $error;
+
+    } else {
+
+      my($newacct) = new FS::svc_acct {
+        'svcpart'  => $pop_svcpart,
+        'username' => $svc_acct_sm->getfield('domuser'),
+        'domsvc'   => $svc_acct_sm->getfield('domsvc'),
+        'dir'      => '/dev/null',
+      };
+      $error = $newacct->insert;
+      die "Error adding svc_acct for " . $newacct->username . " : $error" if $error;
+     
+      my($newforward) = new FS::svc_forward {
+        'svcpart'  => $forward_svcpart, 
+        'srcsvc'   => $newacct->getfield('svcnum'),
+        'dstsvc'   => $def_acct->getfield('svcnum'),
+      };
+      $error = $newforward->insert;
+      die "Error adding svc_forward for " . $newacct->username ." : $error" if $error;
+    }
+     
+    $error = $svc_acct_sm->delete;
+    die "Error deleting svc_acct_sm for " . $svc_acct_sm->domuser ." : $error" if $error;
+
+  };
+
+};
+
+
+$dbh->commit or die $dbh->errstr;
+$dbh->disconnect or die $dbh->errstr;
+
+print "svc_acct_sm records sucessfully migrated\n";
+
+sub usage {
+  die "Usage:\n  fs-migrate-svc_acct_sm user\n"; 
+}
+
diff --git a/bin/fs-radius-add-check b/bin/fs-radius-add-check
new file mode 100755 (executable)
index 0000000..4e4769e
--- /dev/null
@@ -0,0 +1,68 @@
+#!/usr/bin/perl -Tw
+
+# quick'n'dirty hack of fs-setup to add radius attributes
+
+use strict;
+use DBI;
+use FS::UID qw(adminsuidsetup checkeuid getsecrets);
+use FS::raddb;
+
+die "Not running uid freeside!" unless checkeuid();
+
+my %attrib2db =
+  map { lc($FS::raddb::attrib{$_}) => $_ } keys %FS::raddb::attrib;
+
+my $user = shift or die &usage;
+getsecrets($user);
+
+my $dbh = adminsuidsetup $user;
+
+###
+
+print "\n\n", <<END, ":";
+Enter the additional RADIUS check attributes you need to track for
+each user, separated by whitespace.
+END
+my @attributes = map { $attrib2db{lc($_)} or die "unknown attribute $_"; }
+                   split(" ",&getvalue);
+
+sub getvalue {
+  my($x)=scalar(<STDIN>);
+  chop $x;
+  $x;
+}
+
+###
+
+my($char_d) = 80; #default maxlength for text fields
+
+###
+
+foreach my $attribute ( @attributes ) {
+
+  my $statement =
+    "ALTER TABLE svc_acct ADD COLUMN rc_$attribute varchar($char_d) NULL";
+  my $sth = $dbh->prepare( $statement )
+   or warn "Error preparing $statement: ". $dbh->errstr;
+  my $rc = $sth->execute
+    or warn "Error executing $statement: ". $sth->errstr;
+
+  $statement =
+    "ALTER TABLE h_svc_acct ADD COLUMN rc_$attribute varchar($char_d) NULL";
+  $sth = $dbh->prepare( $statement )
+   or warn "Error preparing $statement: ". $dbh->errstr;
+  $rc = $sth->execute
+    or warn "Error executing $statement: ". $sth->errstr;
+
+}
+
+$dbh->commit or die $dbh->errstr;
+
+$dbh->disconnect or die $dbh->errstr;
+
+print "\n\n", "Now you must run dbdef-create.\n\n";
+
+sub usage {
+  die "Usage:\n  fs-radius-add-check user\n"; 
+}
+
diff --git a/bin/fs-radius-add-reply b/bin/fs-radius-add-reply
new file mode 100755 (executable)
index 0000000..3de0137
--- /dev/null
@@ -0,0 +1,69 @@
+#!/usr/bin/perl -Tw
+
+# quick'n'dirty hack of fs-setup to add radius attributes
+
+use strict;
+use DBI;
+use FS::UID qw(adminsuidsetup checkeuid getsecrets);
+use FS::raddb;
+
+die "Not running uid freeside!" unless checkeuid();
+
+my %attrib2db =
+  map { lc($FS::raddb::attrib{$_}) => $_ } keys %FS::raddb::attrib;
+
+my $user = shift or die &usage;
+getsecrets($user);
+
+my $dbh = adminsuidsetup $user;
+
+###
+
+print "\n\n", <<END, ":";
+Enter the additional RADIUS reply attributes you need to track for
+each user, separated by whitespace.
+END
+my @attributes = map { $attrib2db{lc($_)} or die "unknown attribute $_"; }
+                   split(" ",&getvalue);
+
+sub getvalue {
+  my($x)=scalar(<STDIN>);
+  chop $x;
+  $x;
+}
+
+###
+
+my($char_d) = 80; #default maxlength for text fields
+
+###
+
+foreach my $attribute ( @attributes ) {
+
+  my $statement =
+    "ALTER TABLE svc_acct ADD COLUMN radius_$attribute varchar($char_d) NULL";
+  my $sth = $dbh->prepare( $statement )
+    or warn "Error preparing $statement: ". $dbh->errstr;
+  my $rc = $sth->execute
+    or warn "Error executing $statement: ". $sth->errstr;
+
+  $statement =
+    "ALTER TABLE h_svc_acct ADD COLUMN radius_$attribute varchar($char_d) NULL";
+  $sth = $dbh->prepare( $statement )
+    or warn "Error preparing $statement: ". $dbh->errstr;
+  $rc = $sth->execute
+    or warn "Error executing $statement: ". $sth->errstr;
+
+}
+
+$dbh->commit or die $dbh->errstr;
+
+$dbh->disconnect or die $dbh->errstr;
+
+print "\n\n", "Now you must run dbdef-create.\n\n";
+
+sub usage {
+  die "Usage:\n  fs-radius-add-reply user\n"; 
+}
+
+
diff --git a/bin/fs-setup b/bin/fs-setup
deleted file mode 100755 (executable)
index 45332d8..0000000
+++ /dev/null
@@ -1,542 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# create database and necessary tables, etc.  DBI version.
-#
-# ivan@sisd.com 97-nov-8,9
-#
-# agent_type and type_pkgs added.
-# (index need to be declared, & primary keys shoudln't have mysql syntax)
-# ivan@sisd.com 97-nov-13
-#
-# pulled modified version back out of register.cgi ivan@sisd.com 98-feb-21
-#
-# removed extraneous sample data ivan@sisd.com 98-mar-23
-#
-# gained the big hash from dbdef.pm, dbdef.pm usage rewrite ivan@sisd.com
-# 98-apr-19 - 98-may-11 plus
-#
-# finished up ivan@sisd.com 98-jun-1
-#
-# part_svc fields are all forced NULL, not the opposite
-# hmm: also are forced varchar($char_d) as fixed '0' for things like
-# uid is Not Good.  will this break anything else?
-# ivan@sisd.com 98-jun-29
-#
-# ss is 11 chars ivan@sisd.com 98-jul-20
-#
-# setup of arbitrary radius fields ivan@sisd.com 98-aug-9
-#
-# ouch, removed index on company name that wasn't supposed to be there
-# ivan@sisd.com 98-sep-4
-#
-# fix radius attributes ivan@sisd.com 98-sep-27
-
-#to delay loading dbdef until we're ready
-BEGIN { $FS::Record::setup_hack = 1; }
-
-use strict;
-use DBI;
-use FS::dbdef;
-use FS::UID qw(adminsuidsetup datasrc);
-use FS::Record;
-use FS::cust_main_county;
-
-#needs to match FS::Record
-my($dbdef_file) = "/var/spool/freeside/dbdef.". datasrc;
-
-###
-
-print "\nEnter the maximum username length: ";
-my($username_len)=&getvalue;
-
-print "\n\n", <<END, ":";
-Freeside tracks the RADIUS attributes User-Name, Password and Framed-IP-Address
-for each user.  Enter any additional RADIUS attributes you need to track for
-each user, separated by whitespace.
-END
-my @attributes = map { s/\-/_/g; $_; } split(" ",&getvalue);
-
-sub getvalue {
-  my($x)=scalar(<STDIN>);
-  chop $x;
-  $x;
-}
-
-###
-
-my($char_d) = 80; #default maxlength for text fields
-
-#my(@date_type)  = ( 'timestamp', '', ''     );
-my(@date_type)  = ( 'int', 'NULL', ''     );
-my(@perl_type) = ( 'long varchar', 'NULL', ''   ); 
-my(@money_type);
-if (datasrc =~ m/Pg/) { #Pg can't do decimal(10,2)
-  @money_type = ( 'money',   '', '' );
-} else {
-  @money_type = ( 'decimal',   '', '10,2' );
-}
-
-###
-# create a dbdef object from the old data structure
-###
-
-my(%tables)=&tables_hash_hack;
-
-#turn it into objects
-my($dbdef) = new FS::dbdef ( map {  
-  my(@columns);
-  while (@{$tables{$_}{'columns'}}) {
-    my($name,$type,$null,$length)=splice @{$tables{$_}{'columns'}}, 0, 4;
-    push @columns, new FS::dbdef_column ( $name,$type,$null,$length );
-  }
-  FS::dbdef_table->new(
-    $_,
-    $tables{$_}{'primary_key'},
-    #FS::dbdef_unique->new(@{$tables{$_}{'unique'}}),
-    #FS::dbdef_index->new(@{$tables{$_}{'index'}}),
-    FS::dbdef_unique->new($tables{$_}{'unique'}),
-    FS::dbdef_index->new($tables{$_}{'index'}),
-    @columns,
-  );
-} (keys %tables) );
-
-#add radius attributes to svc_acct
-
-my($svc_acct)=$dbdef->table('svc_acct');
-
-my($attribute);
-foreach $attribute (@attributes) {
-  $svc_acct->addcolumn ( new FS::dbdef_column (
-    'radius_'. $attribute,
-    'varchar',
-    'NULL',
-    $char_d,
-  ));
-}
-
-#make part_svc table (but now as object)
-
-my($part_svc)=$dbdef->table('part_svc');
-
-#because of svc_acct_pop
-#foreach (grep /^svc_/, $dbdef->tables) { 
-#foreach (qw(svc_acct svc_acct_sm svc_charge svc_domain svc_wo)) {
-foreach (qw(svc_acct svc_acct_sm svc_domain)) {
-  my($table)=$dbdef->table($_);
-  my($col);
-  foreach $col ( $table->columns ) {
-    next if $col =~ /^svcnum$/;
-    $part_svc->addcolumn( new FS::dbdef_column (
-      $table->name. '__' . $table->column($col)->name,
-      'varchar', #$table->column($col)->type, 
-      'NULL',
-      $char_d, #$table->column($col)->length,
-    ));
-    $part_svc->addcolumn ( new FS::dbdef_column (
-      $table->name. '__'. $table->column($col)->name . "_flag",
-      'char',
-      'NULL',
-      1,
-    ));
-  }
-}
-
-#important
-$dbdef->save($dbdef_file);
-FS::Record::reload_dbdef;
-
-###
-# create 'em
-###
-
-my($dbh)=adminsuidsetup;
-
-#create tables
-$|=1;
-
-my($table);
-foreach  ($dbdef->tables) {
-  my($table)=$dbdef->table($_);
-  print "Creating $_...";
-
-  my($statement);
-
-  #create table
-  foreach $statement ($table->sql_create_table(datasrc)) {
-    #print $statement, "\n"; 
-    $dbh->do( $statement )
-      or die "CREATE error: ",$dbh->errstr, "\ndoing statement: $statement";
-  }
-
-  print "\n";
-}
-
-#not really sample data (and shouldn't default to US)
-
-#cust_main_county
-foreach ( qw(
-AL AK AS AZ AR CA CO CT DC DE FM FL GA GU HI ID IL IN IA KS KY LA
-ME MH MD MA MI MN MS MO MT NC ND NE NH NJ NM NV NY MP OH OK OR PA PW PR RI 
-SC SD TN TX TT UT VT VI VA WA WV WI WY AE AA AP
-) ) {
-  my($cust_main_county)=create FS::cust_main_county({
-    'state' => $_,
-    'tax'   => 0,
-  });  
-  my($error);
-  $error=$cust_main_county->insert;
-  die $error if $error;
-}
-
-$dbh->disconnect or die $dbh->errstr;
-
-###
-# Now it becomes an object.  much better.
-###
-sub tables_hash_hack {
-
-  #note that s/(date|change)/_$1/; to avoid keyword conflict.
-  #put a kludge in FS::Record to catch this or? (pry need some date-handling
-  #stuff anyway also)
-
-  my(%tables)=( #yech.}
-
-    'agent' => {
-      'columns' => [
-        'agentnum', 'int',            '',     '',
-        'agent',    'varchar',           '',     $char_d,
-        'typenum',  'int',            '',     '',
-        'freq',     'smallint',       'NULL', '',
-        'prog',     @perl_type,
-      ],
-      'primary_key' => 'agentnum',
-      'unique' => [ [] ],
-      'index' => [ ['typenum'] ],
-    },
-
-    'agent_type' => {
-      'columns' => [
-        'typenum',   'int',  '', '',
-        'atype',     'varchar', '', $char_d,
-      ],
-      'primary_key' => 'typenum',
-      'unique' => [ [] ],
-      'index' => [ [] ],
-    },
-
-    'type_pkgs' => {
-      'columns' => [
-        'typenum',   'int',  '', '',
-        'pkgpart',   'int',  '', '',
-      ],
-      'primary_key' => '',
-      'unique' => [ ['typenum', 'pkgpart'] ],
-      'index' => [ ['typenum'] ],
-    },
-
-    'cust_bill' => {
-      'columns' => [
-        'invnum',    'int',  '', '',
-        'custnum',   'int',  '', '',
-        '_date',     @date_type,
-        'charged',   @money_type,
-        'owed',      @money_type,
-        'printed',   'int',  '', '',
-      ],
-      'primary_key' => 'invnum',
-      'unique' => [ [] ],
-      'index' => [ ['custnum'] ],
-    },
-
-    'cust_bill_pkg' => {
-      'columns' => [
-        'pkgnum',  'int', '', '',
-        'invnum',  'int', '', '',
-        'setup',   @money_type,
-        'recur',   @money_type,
-        'sdate',   @date_type,
-        'edate',   @date_type,
-      ],
-      'primary_key' => '',
-      'unique' => [ ['pkgnum', 'invnum'] ],
-      'index' => [ ['invnum'] ],
-    },
-
-    'cust_credit' => {
-      'columns' => [
-        'crednum',  'int', '', '',
-        'custnum',  'int', '', '',
-        '_date',    @date_type,
-        'amount',   @money_type,
-        'credited', @money_type,
-        'otaker',   'varchar', '', 8,
-        'reason',   'varchar', '', 255,
-      ],
-      'primary_key' => 'crednum',
-      'unique' => [ [] ],
-      'index' => [ ['custnum'] ],
-    },
-
-    'cust_main' => {
-      'columns' => [
-        'custnum',  'int',  '',     '',
-        'agentnum', 'int',  '',     '',
-        'last',     'varchar', '',     $char_d,
-        'first',    'varchar', '',     $char_d,
-        'ss',       'char', 'NULL', 11,
-        'company',  'varchar', 'NULL', $char_d,
-        'address1', 'varchar', '',     $char_d,
-        'address2', 'varchar', 'NULL', $char_d,
-        'city',     'varchar', '',     $char_d,
-        'county',   'varchar', 'NULL', $char_d,
-        'state',    'char', '',     2,
-        'zip',      'varchar', '',     10,
-        'country',  'char', '',     2,
-        'daytime',  'varchar', 'NULL', 20,
-        'night',    'varchar', 'NULL', 20,
-        'fax',      'varchar', 'NULL', 12,
-        'payby',    'char', '',     4,
-        'payinfo',  'varchar', 'NULL', 16,
-        'paydate',  @date_type,
-        'payname',  'varchar', 'NULL', $char_d,
-        'tax',      'char', 'NULL', 1,
-        'otaker',   'varchar', '',     8,
-        'refnum',   'int',  '',     '',
-      ],
-      'primary_key' => 'custnum',
-      'unique' => [ [] ],
-      #'index' => [ ['last'], ['company'] ],
-      'index' => [ ['last'], ],
-    },
-
-    'cust_main_county' => { #county+state are checked off the cust_main_county
-                            #table for validation and to provide a tax rate.
-                            #add country?
-      'columns' => [
-        'taxnum',   'int',   '',    '',
-        'state',    'char',  '',    2,  #two letters max in US... elsewhere?
-        'county',   'varchar',  '',    $char_d,
-        'tax',      'real',  '',    '', #tax %
-      ],
-      'primary_key' => 'taxnum',
-      'unique' => [ [] ],
-  #    'unique' => [ ['taxnum'], ['state', 'county'] ],
-      'index' => [ [] ],
-    },
-
-    'cust_pay' => {
-      'columns' => [
-        'paynum',   'int',    '',   '',
-        'invnum',   'int',    '',   '',
-        'paid',     @money_type,
-        '_date',    @date_type,
-        'payby',    'char',   '',     4, # CARD/BILL/COMP, should be index into
-                                         # payment type table.
-        'payinfo',  'varchar',   'NULL', 16,  #see cust_main above
-        'paybatch', 'varchar',   'NULL', $char_d, #for auditing purposes.
-      ],
-      'primary_key' => 'paynum',
-      'unique' => [ [] ],
-      'index' => [ ['invnum'] ],
-    },
-
-    'cust_pay_batch' => { #what's this used for again?  list of customers
-                          #in current CARD batch? (necessarily CARD?)
-      'columns' => [
-        'invnum',   'int',    '',   '',
-        'custnum',   'int',    '',   '',
-        'last',     'varchar', '',     $char_d,
-        'first',    'varchar', '',     $char_d,
-        'address1', 'varchar', '',     $char_d,
-        'address2', 'varchar', 'NULL', $char_d,
-        'city',     'varchar', '',     $char_d,
-        'state',    'char', '',     2,
-        'zip',      'varchar', '',     10,
-        'country',  'char', '',     2,
-        'trancode', 'TINYINT', '', '',
-        'cardnum',  'varchar', '',     16,
-        'exp',      @date_type,
-        'payname',  'varchar', 'NULL', $char_d,
-        'amount',   @money_type,
-      ],
-      'primary_key' => '',
-      'unique' => [ [] ],
-      'index' => [ ['invnum'], ['custnum'] ],
-    },
-
-    'cust_pkg' => {
-      'columns' => [
-        'pkgnum',    'int',    '',   '',
-        'custnum',   'int',    '',   '',
-        'pkgpart',   'int',    '',   '',
-        'otaker',    'varchar', '', 8,
-        'setup',     @date_type,
-        'bill',      @date_type,
-        'susp',      @date_type,
-        'cancel',    @date_type,
-        'expire',    @date_type,
-      ],
-      'primary_key' => 'pkgnum',
-      'unique' => [ [] ],
-      'index' => [ ['custnum'] ],
-    },
-
-    'cust_refund' => {
-      'columns' => [
-        'refundnum',    'int',    '',   '',
-        'crednum',      'int',    '',   '',
-        '_date',        @date_type,
-        'refund',       @money_type,
-        'otaker',       'varchar',   '',   8,
-        'reason',       'varchar',   '',   $char_d,
-        'payby',        'char',   '',     4, # CARD/BILL/COMP, should be index
-                                             # into payment type table.
-        'payinfo',      'varchar',   'NULL', 16,  #see cust_main above
-      ],
-      'primary_key' => 'refundnum',
-      'unique' => [ [] ],
-      'index' => [ ['crednum'] ],
-    },
-
-    'cust_svc' => {
-      'columns' => [
-        'svcnum',    'int',    '',   '',
-        'pkgnum',    'int',    '',   '',
-        'svcpart',   'int',    '',   '',
-      ],
-      'primary_key' => 'svcnum',
-      'unique' => [ [] ],
-      'index' => [ ['svcnum'], ['pkgnum'], ['svcpart'] ],
-    },
-
-    'part_pkg' => {
-      'columns' => [
-        'pkgpart',    'int',    '',   '',
-        'pkg',        'varchar',   '',   $char_d,
-        'comment',    'varchar',   '',   $char_d,
-        'setup',      @perl_type,
-        'freq',       'smallint', '', '',  #billing frequency (months)
-        'recur',      @perl_type,
-      ],
-      'primary_key' => 'pkgpart',
-      'unique' => [ [] ],
-      'index' => [ [] ],
-    },
-
-    'pkg_svc' => {
-      'columns' => [
-        'pkgpart',    'int',    '',   '',
-        'svcpart',    'int',    '',   '',
-        'quantity',   'int',    '',   '',
-      ],
-      'primary_key' => '',
-      'unique' => [ ['pkgpart', 'svcpart'] ],
-      'index' => [ ['pkgpart'] ],
-    },
-
-    'part_referral' => {
-      'columns' => [
-        'refnum',   'int',    '',   '',
-        'referral', 'varchar',   '',   $char_d,
-      ],
-      'primary_key' => 'refnum',
-      'unique' => [ [] ],
-      'index' => [ [] ],
-    },
-
-    'part_svc' => {
-      'columns' => [
-        'svcpart',    'int',    '',   '',
-        'svc',        'varchar',   '',   $char_d,
-        'svcdb',      'varchar',   '',   $char_d,
-      ],
-      'primary_key' => 'svcpart',
-      'unique' => [ [] ],
-      'index' => [ [] ],
-    },
-
-    #(this should be renamed to part_pop)
-    'svc_acct_pop' => {
-      'columns' => [
-        'popnum',    'int',    '',   '',
-        'city',      'varchar',   '',   $char_d,
-        'state',     'char',   '',   2,
-        'ac',        'char',   '',   3,
-        'exch',      'char',   '',   3,
-        #rest o' number?
-      ],
-      'primary_key' => 'popnum',
-      'unique' => [ [] ],
-      'index' => [ [] ],
-    },
-
-    'svc_acct' => {
-      'columns' => [
-        'svcnum',    'int',    '',   '',
-        'username',  'varchar',   '',   $username_len, #unique (& remove dup code)
-        '_password', 'varchar',   '',   25, #13 for encryped pw's plus ' *SUSPENDED*
-        'popnum',    'int',    'NULL',   '',
-        'uid',       'bigint', 'NULL',   '',
-        'gid',       'bigint', 'NULL',   '',
-        'finger',    'varchar',   'NULL',   $char_d,
-        'dir',       'varchar',   'NULL',   $char_d,
-        'shell',     'varchar',   'NULL',   $char_d,
-        'quota',     'varchar',   'NULL',   $char_d,
-        'slipip',    'varchar',   'NULL',   15, #four TINYINTs, bah.
-      ],
-      'primary_key' => 'svcnum',
-      'unique' => [ [] ],
-      'index' => [ ['username'] ],
-    },
-
-    'svc_acct_sm' => {
-      'columns' => [
-        'svcnum',    'int',    '',   '',
-        'domsvc',    'int',    '',   '',
-        'domuid',    'bigint', '',   '',
-        'domuser',   'varchar',   '',   $char_d,
-      ],
-      'primary_key' => 'svcnum',
-      'unique' => [ [] ],
-      'index' => [ ['domsvc'], ['domuid'] ], 
-    },
-
-    #'svc_charge' => {
-    #  'columns' => [
-    #    'svcnum',    'int',    '',   '',
-    #    'amount',    @money_type,
-    #  ],
-    #  'primary_key' => 'svcnum',
-    #  'unique' => [ [] ],
-    #  'index' => [ [] ],
-    #},
-
-    'svc_domain' => {
-      'columns' => [
-        'svcnum',    'int',    '',   '',
-        'domain',    'varchar',    '',   $char_d,
-      ],
-      'primary_key' => 'svcnum',
-      'unique' => [ ['domain'] ],
-      'index' => [ [] ],
-    },
-
-    #'svc_wo' => {
-    #  'columns' => [
-    #    'svcnum',    'int',    '',   '',
-    #    'svcnum',    'int',    '',   '',
-    #    'svcnum',    'int',    '',   '',
-    #    'worker',    'varchar',   '',   $char_d,
-    #    '_date',     @date_type,
-    #  ],
-    #  'primary_key' => 'svcnum',
-    #  'unique' => [ [] ],
-    #  'index' => [ [] ],
-    #},
-
-  );
-
-  %tables;
-
-}
-
diff --git a/bin/generate-prepay b/bin/generate-prepay
new file mode 100755 (executable)
index 0000000..cb4ba7f
--- /dev/null
@@ -0,0 +1,35 @@
+#!/usr/bin/perl -w
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::prepay_credit;
+
+require 5.004; #srand(time|$$);
+
+my $user = shift or die &usage;
+&adminsuidsetup( $user );
+
+my $amount = shift or die &usage;
+
+my $seconds = shift or die &usage;
+
+my $num_digits = shift or die &usage;
+
+my $num_entries = shift or die &usage;
+
+for ( 1 .. $num_entries ) {
+  my $identifier = join( '', map int(rand(10)), ( 1 .. $num_digits ) );
+  my $prepay_credit = new FS::prepay_credit {
+    'identifier' => $identifier,
+    'amount'     => $amount,
+    'seconds'    => $seconds,
+  };
+  my $error = $prepay_credit->insert;
+  die $error if $error;
+  print "$identifier\n";
+}
+
+sub usage {
+  die "Usage:\n\n  generate-prepay user amount seconds num_digits num_entries";
+}
+
diff --git a/bin/generate-raddb b/bin/generate-raddb
new file mode 100755 (executable)
index 0000000..1d0053a
--- /dev/null
@@ -0,0 +1,37 @@
+#!/usr/bin/perl
+
+# usage: generate-raddb radius-server/raddb/dictionary* >raddb.pm
+#  i.e.: generate-raddb ~/src/freeradius-0.2/raddb/dictionary* >FS/raddb.pm
+
+print <<END;
+package FS::raddb;
+use vars qw(%attrib);
+
+%attrib = (
+END
+
+while (<>) {
+  next if /^(#|\s*$|\$INCLUDE\s+)/;
+  next if /^(VALUE|VENDOR|BEGIN\-VENDOR|END\-VENDOR)\s+/;
+  /^(ATTRIBUTE|ATTRIB_NMC)\s+([\w\-]+)\s+/ or die $_;
+  $attrib = $2;
+  $dbname = lc($2);
+  $dbname =~ s/\-/_/g;
+  $hash{$dbname} = $attrib;
+  #print "$2\n";
+}
+
+foreach ( keys %hash ) {
+#  print "$_\n" if length($_)>24;
+#  print substr($_,0,24),"\n" if length($_)>24; 
+#  $max = length($_) if length($_)>$max;
+#everything >24 is still unique, at least with freeradius comprehensive dataset
+  print "  '". substr($_,0,24). "' => '$hash{$_}',\n";
+}
+
+print <<END;
+);
+
+1;
+END
+
diff --git a/bin/generate-tests b/bin/generate-tests
new file mode 100755 (executable)
index 0000000..73fd29e
--- /dev/null
@@ -0,0 +1,21 @@
+#!/usr/bin/perl
+@files = glob('FS/*.pm');
+foreach (@files) {
+#  warn $_;
+  chomp;
+  s/^FS\///;
+  $f=$_;
+  $f=~s/pm$/t/;
+  $m=$_;
+  $m=~s/\.pm$//;
+  open(TEST,">t/$f");
+  print "t/$f\n";
+  print TEST
+             'BEGIN { $| = 1; print "1..1\n" }'. "\n".
+             'END {print "not ok 1\n" unless $loaded;}'. "\n".
+             "use FS::$m;\n".
+             '$loaded=1;'. "\n".
+             'print "ok 1\n";'. "\n"
+             ;
+  close TEST;
+}
diff --git a/bin/masonize b/bin/masonize
new file mode 100755 (executable)
index 0000000..475c9a6
--- /dev/null
@@ -0,0 +1,70 @@
+#!/usr/bin/perl
+
+foreach $file ( split(/\n/, `find . -depth -print | grep cgi\$`) ) {
+  open(F,$file) or die "can't open $file for reading: $!";
+  @file = <F>;
+  #print "$file ". scalar(@file). "\n";
+  close $file;
+  system("chmod u+w $file");
+  open(W,">$file") or die "can't open $file for writing: $!";
+  select W; $| = 1; select STDOUT;
+  $all = join('',@file);
+
+  $mode = 'html';
+  while ( length($all) ) {
+
+    if ( $mode eq 'html' ) {
+
+      if ( $all =~ /^(.+?)(<%=?.*)$/s && $1 !~ /<%/s ) {
+        print W $1;
+        $all = $2;
+        next;
+      } elsif ( $all =~ /^<%=(.*)$/s ) {
+        print W '<%';
+        $all = $1;
+        $mode = 'perlv';
+        #die;
+        next;
+      } elsif ( $all =~ /^<%(.*)$/s ) {
+        print W "\n";
+        $all = $1;
+        $mode = 'perlc';
+        next;
+      } elsif ( $all !~ /<%/s ) {
+        print W $all;
+        last;
+      } else {
+        warn length($all); die;
+      }
+      die;
+
+    } elsif ( $mode eq 'perlv' ) {
+
+      if ( $all =~ /^(.*?%>)(.*)$/s ) {
+        print W $1;
+        $all=$2;
+        $mode = 'html';
+        next;
+      }
+      die 'unterminated <%= ???';
+
+    } elsif ( $mode eq 'perlc' ) {
+
+      if ( $all =~ /^([^\n]*?)%>(.*)$/s ) {
+        print W "%$1\n";
+        $all=$2;
+        $mode='html';
+        next;
+      }
+      if ( $all =~ /^([^\n]*)\n(.*)$/s ) {
+        print W "%$1\n";
+        $all=$2;
+        next;
+      }
+
+    } else { die };
+
+  }
+
+  close W;
+}
diff --git a/bin/passwd.import b/bin/passwd.import
new file mode 100755 (executable)
index 0000000..093f8ba
--- /dev/null
@@ -0,0 +1,120 @@
+#!/usr/bin/perl -Tw
+# $Id: passwd.import,v 1.8 2003-06-12 14:08:00 ivan Exp $
+
+use strict;
+use vars qw(%part_svc);
+use Date::Parse;
+use Term::Query qw(query);
+use Net::SCP qw(iscp);
+use FS::UID qw(adminsuidsetup datasrc);
+use FS::Record qw(qsearch qsearchs);
+use FS::svc_acct;
+use FS::part_svc;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+push @FS::svc_acct::shells, qw(/bin/sync /sbin/shutdown /bin/halt /sbin/halt); #others?
+
+my($spooldir)="/usr/local/etc/freeside/export.". datasrc;
+
+#$FS::svc_acct::nossh_hack = 1;
+$FS::svc_Common::noexport_hack = 1;
+
+###
+
+%part_svc=map { $_->svcpart, $_ } qsearch('part_svc',{'svcdb'=>'svc_acct'});
+
+die "No services with svcdb svc_acct!\n" unless %part_svc;
+
+print "\n\n", &menu_svc, "\n", <<END;
+Enter part number to import.
+END
+my($shell_svcpart)=&getpart;
+
+print "\n\n", <<END;
+Enter the location and name of your _user_ passwd file, for example
+"mail.isp.com:/etc/passwd" or "nis.isp.com:/etc/global/passwd"
+END
+my($loc_passwd)=&getvalue(":");
+iscp("root\@$loc_passwd", "$spooldir/passwd.import");
+
+print "\n\n", <<END;
+Enter the location and name of your _user_ shadow file, for example
+"mail.isp.com:/etc/shadow" or "bsd.isp.com:/etc/master.passwd"
+END
+my($loc_shadow)=&getvalue(":");
+iscp("root\@$loc_shadow", "$spooldir/shadow.import");
+
+sub menu_svc {
+  ( join "\n", map "$_: ".$part_svc{$_}->svc, sort keys %part_svc ). "\n";
+}
+sub getpart {
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query "Enter part number:", 'irk', [ keys %part_svc ];
+  $^W=1;
+  $return;
+}
+sub getvalue {
+  my $prompt = shift;
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query $prompt, '';
+  $^W=1;
+  $return;
+}
+
+print "\n\n";
+
+###
+
+open(PASSWD,"<$spooldir/passwd.import");
+open(SHADOW,"<$spooldir/shadow.import");
+
+my(%password);
+while (<SHADOW>) {
+  chop;
+  my($username,$password)=split(/:/);
+  #$password =~ s/^\!$/\*/;
+  #$password =~ s/\!+/\*SUSPENDED\* /;
+  $password{$username}=$password;
+}
+
+while (<PASSWD>) {
+  chop;
+  my($username,$x,$uid,$gid,$finger,$dir,$shell) = split(/:/);
+  my $password = $password{$username};
+
+  my $svcpart = $shell_svcpart;
+
+  #if ( qsearchs('svc_acct', { 'username' => $username } ) ) {
+  #  warn "warning: $username already exists; skipping\n";
+  #  next;
+  #}
+
+  my($svc_acct) = new FS::svc_acct ({
+    'svcpart'   => $svcpart,
+    'username'  => $username,
+    '_password' => $password,
+    'uid'       => $uid,
+    'gid'       => $gid,
+    'finger'    => $finger,
+    'dir'       => $dir,
+    'shell'     => $shell,
+    #%{$allparam{$username}},
+  });
+  my($error);
+  $error=$svc_acct->insert;
+  if ( $error ) {
+    if ( $error =~ /duplicate/i ) {
+      warn "$username: $error";
+    } else {
+      die "$username: $error";
+    }
+  }
+
+}
+
+sub usage {
+  die "Usage:\n\n  passwd.import user\n";
+}
+
index 1edb1c4..46ccc77 100755 (executable)
--- a/bin/pod2x
+++ b/bin/pod2x
@@ -3,21 +3,54 @@
 #use Pod::Text;
 #$Pod::Text::termcap=1;
 
-my $site_perl = "./site_perl";
+my $site_perl = "./FS";
 #my $catman = "./catman";
-my $catman = "./htdocs/docs/man";
+#my $catman = "./htdocs/docs/man";
 #my $html = "./htdocs/docs/man";
+my $html = "./httemplate/docs/man";
 
 $|=1;
 
-die "Can't find $site_perl and $catman"
-  unless [ -d $site_perl ] && [ -d $catman ] && [ -d $html ];
+die "Can't find $site_perl" unless -d $site_perl;
+#die "Can't find $catman" unless -d $catman;
+die "Can't find $html" unless -d $html;
 
-foreach my $file (glob("$site_perl/*.pm")) {
-  $file =~ /\/([\w\-]+)\.pm$/ or die "oops file $file";
-  my $name = $1;
-  print "$name\n"; 
-  system "pod2text $file >$catman/$name.txt"; 
-#  system "pod2html --podpath=$site_perl $file >$html/$name.html";
+#make some useless links
+foreach my $file (
+  glob("$site_perl/bin/freeside-*"),
+) {
+  next if $file =~ /\.pod$/;
+  #symlink $file, "$file.pod"; # or die "link $file to $file.pod: $!";
+  system("cp $file $file.pod");
+}
+
+foreach my $file (
+  glob("$site_perl/*.pm"),
+  glob("$site_perl/*/*.pm"),
+  glob("$site_perl/*/*/*.pm"),
+  glob("$site_perl/bin/*.pod"),
+  glob("./fs_sesmon/FS-SessionClient/*.pm"),
+  glob("./fs_signup/FS-SignupClient/*.pm"),
+  glob("./fs_selfadmin/FS-MailAdminServer/*.pm"),
+) {
+  next if $file =~ /(^|\/)blib\//;
+  #$file =~ /\/([\w\-]+)\.pm$/ or die "oops file $file";
+  my $name;
+  if ( $file =~ /fs_\w+\/FS\-\w+\/(.*)\.pm$/ ) {
+    $name = "FS/$1";
+  } elsif ( $file =~ /$site_perl\/(.*)\.(pm|pod)$/ ) {
+    $name = $1;
+  } else {
+    die "oops file $file";
+  }
+  print "$name\n";
+  my $htmlroot = join('/', map '..',1..(scalar($file =~ tr/\///)-2)) || '.';
+#  system "pod2text $file >$catman/$name.txt"; 
+  system "pod2html --podroot=$site_perl --podpath=./FS:./FS/UI:.:./bin --norecurse --htmlroot=$htmlroot $file >$html/$name.html";
+  #system "pod2html --podroot=$site_perl --htmlroot=$htmlroot $file >$html/$name.html";
 #  system "pod2html $file >$html/$name.html";
 }
+
+#remove the useless links
+unlink glob("$site_perl/bin/*.pod");
+
diff --git a/bin/populate-msgcat b/bin/populate-msgcat
new file mode 100755 (executable)
index 0000000..719d330
--- /dev/null
@@ -0,0 +1,127 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+use FS::msgcat;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+foreach my $del_msgcat ( qsearch('msgcat', {}) ) {
+  my $error = $del_msgcat->delete;
+  die $error if $error;
+}
+
+my %messages = messages();
+
+foreach my $msgcode ( keys %messages ) {
+  foreach my $locale ( keys %{$messages{$msgcode}} ) {
+    my $msgcat = new FS::msgcat( {
+      'msgcode' => $msgcode,
+      'locale'  => $locale,
+      'msg'     => $messages{$msgcode}{$locale},
+    });
+    my $error = $msgcat->insert;
+    die $error if $error;
+  }
+}
+
+#print "Message catalog initialized sucessfully\n";
+
+sub messages {
+
+  #  'msgcode' => {
+  #    'en_US' => 'Message',
+  #  },
+
+  (
+
+    'passwords_dont_match' => {
+      'en_US' => "Passwords don't match",
+    },
+
+    'invalid_card' => {
+      'en_US' => 'Invalid credit card number',
+    },
+
+    'unknown_card_type' => {
+      'en_US' => 'Unknown card type',
+    },
+
+    'not_a' => {
+      'en_US' => 'Not a ',
+    },
+
+    'empty_password' => {
+      'en_US' => 'Empty password',
+    },
+
+    'no_access_number_selected' => {
+      'en_US' => 'No access number selected',
+    },
+
+    'illegal_text' => {
+      'en_US' => 'Illegal (text)',
+      #'en_US' => 'Only letters, numbers, spaces, and the following punctuation symbols are permitted: ! @ # $ % & ( ) - + ; : \' " , . ? / in field',
+    },
+
+    'illegal_or_empty_text' => {
+      'en_US' => 'Illegal or empty (text)',
+      #'en_US' => 'Only letters, numbers, spaces, and the following punctuation symbols are permitted: ! @ # $ % & ( ) - + ; : \' " , . ? / in required field',
+    },
+
+    'illegal_username' => {
+      'en_US' => 'Illegal username',
+    },
+
+    'illegal_password' => {
+      'en_US' => 'Illegal password (',
+    },
+
+    'illegal_password_characters' => {
+      'en_US' => ' characters)',
+    },
+
+    'username_in_use' => {
+      'en_US' => 'Username in use',
+    },
+
+    'illegal_email_invoice_address' => {
+      'en_US' => 'Illegal email invoice address',
+    },
+
+    'illegal_name' => {
+      'en_US' => 'Illegal (name)',
+      #'en_US' => 'Only letters, numbers, spaces and the following punctuation symbols are permitted: , . - \' in field',
+    },
+
+    'illegal_phone' => {
+      'en_US' => 'Illegal (phone)',
+      #'en_US' => '',
+    },
+
+    'illegal_zip' => {
+      'en_US' => 'Illegal (zip)',
+      #'en_US' => '',
+    },
+
+    'expired_card' => {
+      'en_US' => 'Expired card',
+    },
+
+    'daytime' => {
+      'en_US' => 'Day Phone',
+    },
+
+    'night' => {
+      'en_US' => 'Night Phone',
+    },
+
+  );
+}
+
+sub usage {
+  die "Usage:\n\n  populate-msgcat user\n";
+}
+
index 3f65a08..0bc370f 100755 (executable)
-#!/usr/bin/perl -Tw
+#!/usr/bin/perl -w
 #
-# Create and export password files: passwd, passwd.adjunct, shadow,
-# acp_passwd, acp_userinfo, acp_dialup, users
+# $Id: svc_acct.export,v 1.36 2002-05-16 14:28:35 ivan Exp $
 #
-# ivan@voicenet.com late august/september 96
-# (the password encryption bits were from melody)
-#
-# use a temporary copy of svc_acct to minimize lock time on the real file,
-# and skip blank entries.
-#
-# ivan@voicenet.com 96-Oct-6
-#
-# change users / acp_dialup file formats
-# ivan@voicenet.com 97-jan-28-31
-#
-# change priority (after copies) to 19, not 10
-# ivan@voicenet.com 97-feb-5
-#
-# added exit if stuff is already locked 97-apr-15
-#
-# rewrite ivan@sisd.com 98-mar-9
-#
-# Changed 'password' to '_password' because Pg6.3 reserves this word
-# Added code to create a FreeBSD style master.passwd file
-#   bmccane@maxbaud.net 98-Apr-3
-#
-# don't export non-root 0 UID's, even if they get put in the database
-# ivan@sisd.com 98-jul-14
-#
-# Uses Idle_Timeout, Port_Limit, Framed_Netmask and Framed_Route if they
-# exist; need some way to support arbitrary radius fields.  also 
-# /var/spool/freeside/conf/ ivan@sisd.com 98-jul-26, aug-9
-#
-# OOPS!  added arbitrary radius fields (pry 98-aug-16) but forgot to say so.
-# ivan@sisd.com 98-sep-18
+# Create and export password, radius and vpopmail password files:
+# passwd, passwd.adjunct, shadow, acp_passwd, acp_userinfo, acp_dialup
+# users/assign, domains/vdomain/vpasswd
+# Also export sendmail and qmail config files.
 
 use strict;
+use vars qw($conf);
 use Fcntl qw(:flock);
-use FS::SSH qw(scp ssh);
-use FS::UID qw(adminsuidsetup);
-use FS::Record qw(qsearch fields);
-
-my($fshellmachines)="/var/spool/freeside/conf/shellmachines";
-my(@shellmachines);
-if ( -e $fshellmachines ) {
-  open(SHELLMACHINES,$fshellmachines);
-  @shellmachines=map {
-    /^(.*)$/ or die "Illegal line in conf/shellmachines"; #we trust the file
-    $1;
-  } grep $_ !~ /^(#|$)/, <SHELLMACHINES>;
-  close SHELLMACHINES;
-}
+use File::Path;
+use IO::Handle;
+use FS::Conf;
+use Net::SSH qw(ssh);
+use Net::SCP qw(scp);
+use FS::UID qw(adminsuidsetup datasrc dbh);
+use FS::Record qw(qsearch qsearchs fields);
+use FS::svc_acct;
+use FS::svc_domain;
+use FS::svc_forward;
 
-my($fbsdshellmachines)="/var/spool/freeside/conf/bsdshellmachines";
-my(@bsdshellmachines);
-if ( -e $fbsdshellmachines ) {
-  open(BSDSHELLMACHINES,$fbsdshellmachines);
-  @bsdshellmachines=map {
-    /^(.*)$/ or die "Illegal line in conf/bsdshellmachines"; #we trust the file
-    $1;
-  } grep $_ !~ /^(#|$)/, <BSDSHELLMACHINES>;
-  close BSDSHELLMACHINES;
-}
+my $ssh='ssh';
+my $rsync='rsync';
 
-my($fnismachines)="/var/spool/freeside/conf/nismachines";
-my(@nismachines);
-if ( -e $fnismachines ) {
-  open(NISMACHINES,$fnismachines);
-  @nismachines=map {
-    /^(.*)$/ or die "Illegal line in conf/nismachines"; #we trust the file
-    $1;
-  } grep $_ !~ /^(#|$)/, <NISMACHINES>;
-  close NISMACHINES;
-}
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+$conf = new FS::Conf;
+
+my $userpolicy = $conf->config('username_policy')
+  if $conf->exists('username_policy');
+
+my @shellmachines = $conf->config('shellmachines')
+  if $conf->exists('shellmachines');
+
+my @bsdshellmachines = $conf->config('bsdshellmachines')
+  if $conf->exists('bsdshellmachines');
+
+my @nismachines = $conf->config('nismachines')
+  if $conf->exists('nismachines');
+
+my @erpcdmachines = $conf->config('erpcdmachines')
+  if $conf->exists('erpcdmachines');
+
+my @radiusmachines = $conf->config('radiusmachines')
+  if $conf->exists('radiusmachines');
+
+my $textradiusprepend =
+  $conf->exists('textradiusprepend')
+    ? $conf->config('textradiusprepend')
+    : '';
 
-my($ferpcdmachines)="/var/spool/freeside/conf/erpcdmachines";
-my(@erpcdmachines);
-if ( -e $ferpcdmachines ) {
-  open(ERPCDMACHINES,$ferpcdmachines);
-  @erpcdmachines=map {
-    /^(.*)$/ or die "Illegal line in conf/erpcdmachines"; #we trust the file
-    $1;
-  } grep $_ !~ /^(#|$)/, <ERPCDMACHINES>;
-  close ERPCDMACHINES;
+warn "using depriciated textradiusprepend file" if $textradiusprepend;
+
+
+my $radiusprepend =
+  $conf->exists('radiusprepend')
+    ? join("\n", $conf->config('radiusprepend'))
+    : '';
+
+my @vpopmailmachines = $conf->config('vpopmailmachines')
+  if $conf->exists('vpopmailmachines');
+my $vpopmailrestart = '';
+$vpopmailrestart = $conf->config('vpopmailrestart')
+  if $conf->exists('vpopmailrestart');
+
+my ($machine, $vpopdir, $vpopuid, $vpopgid) = split (/\s+/, $vpopmailmachines[0]) if $vpopmailmachines[0];
+
+my($shellmachine, @qmailmachines);
+if ( $conf->exists('qmailmachines') ) {
+  $shellmachine = $conf->config('shellmachine');
+  @qmailmachines = $conf->config('qmailmachines');
 }
 
-my($fradiusmachines)="/var/spool/freeside/conf/radiusmachines";
-my(@radiusmachines);
-if ( -e $fradiusmachines ) {
-  open(RADIUSMACHINES,$fradiusmachines);
-  @radiusmachines=map {
-    /^(.*)$/ or die "Illegal line in conf/radiusmachines"; #we trust the file
-    $1;
-  } grep $_ !~ /^(#|$)/, <RADIUSMACHINES>;
-  close RADIUSMACHINES;
+my(@sendmailmachines, $sendmailconfigpath, $sendmailrestart);
+if ( $conf->exists('sendmailmachines') ) {
+  @sendmailmachines = $conf->config('sendmailmachines');
+  $sendmailconfigpath = $conf->config('sendmailconfigpath') || '/etc';
+  $sendmailrestart = $conf->config('sendmailrestart');
 }
 
-my($spooldir)="/var/spool/freeside/export";
-my($spoollock)="/var/spool/freeside/svc_acct.export.lock";
+my $mydomain = $conf->config('domain') if $conf->exists('domain');
+
+
 
-adminsuidsetup;
 
 my(@saltset)= ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
-srand(time|$$);
+require 5.004; #srand(time|$$);
+
+my $spooldir = "/usr/local/etc/freeside/export.". datasrc;
+my $spoollock = "/usr/local/etc/freeside/svc_acct.export.lock.". datasrc;
 
 open(EXPORT,"+>>$spoollock") or die "Can't open $spoollock: $!";
 select(EXPORT); $|=1; select(STDOUT);
@@ -110,159 +98,368 @@ unless ( flock(EXPORT,LOCK_EX|LOCK_NB) ) {
   seek(EXPORT,0,0);
   my($pid)=<EXPORT>;
   chop($pid);
-  #no reason to start loct of blocking processes
+  #no reason to start lots of blocking processes
   die "Is another export process running under pid $pid?\n";
 }
 seek(EXPORT,0,0);
 print EXPORT $$,"\n";
 
-my(@svc_acct)=qsearch('svc_acct',{});
+my(@svc_domain)=qsearch('svc_domain',{});
 
 ( open(MASTER,">$spooldir/master.passwd")
-  and flock(MASTER,LOCK_EX|LOCK_NB)
-) or die "Can't open $spooldir/master.passwd: $!";
+  and flock(MASTER,LOCK_EX|LOCK_NB)  
+) or die "Can't open $spooldir/.master.passwd: $!";
 ( open(PASSWD,">$spooldir/passwd")
   and flock(PASSWD,LOCK_EX|LOCK_NB)  
 ) or die "Can't open $spooldir/passwd: $!";
 ( open(SHADOW,">$spooldir/shadow")
-  and flock(SHADOW,LOCK_EX|LOCK_NB) 
+  and flock(SHADOW,LOCK_EX|LOCK_NB)  
 ) or die "Can't open $spooldir/shadow: $!";
-( open(ACP_PASSWD,">$spooldir/acp_passwd") 
-  and flock (ACP_PASSWD,LOCK_EX|LOCK_NB)
+( open(ACP_PASSWD,">$spooldir/acp_passwd")
+  and flock(ACP_PASSWD,LOCK_EX|LOCK_NB)  
 ) or die "Can't open $spooldir/acp_passwd: $!";
-( open (ACP_DIALUP,">$spooldir/acp_dialup")
-  and flock(ACP_DIALUP,LOCK_EX|LOCK_NB)
+( open(ACP_DIALUP,">$spooldir/acp_dialup")
+  and flock(ACP_DIALUP,LOCK_EX|LOCK_NB)  
 ) or die "Can't open $spooldir/acp_dialup: $!";
-( open (USERS,">$spooldir/users")
-  and flock(USERS,LOCK_EX|LOCK_NB)
+( open(USERS,">$spooldir/users")
+  and flock(USERS,LOCK_EX|LOCK_NB)  
 ) or die "Can't open $spooldir/users: $!";
 
+( open(ASSIGN,">$spooldir/assign")
+  and flock(ASSIGN,LOCK_EX|LOCK_NB)  
+) or die "Can't open $spooldir/assign: $!";
+( open(RCPTHOSTS,">$spooldir/rcpthosts")
+  and flock(RCPTHOSTS,LOCK_EX|LOCK_NB) 
+) or die "Can't open $spooldir/rcpthosts: $!";
+( open(VPOPRCPTHOSTS,">$spooldir/vpoprcpthosts")
+  and flock(VPOPRCPTHOSTS,LOCK_EX|LOCK_NB) 
+) or die "Can't open $spooldir/rcpthosts: $!";
+( open(RECIPIENTMAP,">$spooldir/recipientmap") 
+  and flock(RECIPIENTMAP,LOCK_EX|LOCK_NB) 
+) or die "Can't open $spooldir/recipientmap: $!";
+( open(VIRTUALDOMAINS,">$spooldir/virtualdomains") 
+  and flock(VIRTUALDOMAINS,LOCK_EX|LOCK_NB)
+) or die "Can't open $spooldir/virtualdomains: $!";
+( open(VPOPVIRTUALDOMAINS,">$spooldir/vpopvirtualdomains") 
+  and flock(VPOPVIRTUALDOMAINS,LOCK_EX|LOCK_NB)
+) or die "Can't open $spooldir/virtualdomains: $!";
+( open(VIRTUSERTABLE,">$spooldir/virtusertable")
+  and flock(VIRTUSERTABLE,LOCK_EX|LOCK_NB)
+) or die "Can't open $spooldir/virtusertable: $!";
+( open(SENDMAIL_CW,">$spooldir/sendmail.cw")
+  and flock(SENDMAIL_CW,LOCK_EX|LOCK_NB)
+) or die "Can't open $spooldir/sendmail.cw: $!";
+
+
+
 chmod 0644, "$spooldir/passwd",
             "$spooldir/acp_dialup",
+            "$spooldir/assign",
+            "$spooldir/sendmail.cw",
+            "$spooldir/virtusertable",
+            "$spooldir/rcpthosts",
+            "$spooldir/vpoprcpthosts",
+            "$spooldir/recipientmap",
+            "$spooldir/virtualdomains",
+            "$spooldir/vpopvirtualdomains",
+
 ;
 chmod 0600, "$spooldir/master.passwd",
-           "$spooldir/acp_passwd",
+            "$spooldir/acp_passwd",
             "$spooldir/shadow",
             "$spooldir/users",
 ;
 
-setpriority(0,0,10);
+rmtree"$spooldir/domains", 0, 1;
+mkdir "$spooldir/domains", 0700;
 
-my($svc_acct);
-foreach $svc_acct (@svc_acct) {
-
-  my($password)=$svc_acct->getfield('_password');
-  my($cpassword,$rpassword);
-  if ( ( length($password) <= 8 )
-       && ( $password ne '*' )
-       && ( $password ne '' )
-     ) {
-    $cpassword=crypt($password,
-                     $saltset[int(rand(64))].$saltset[int(rand(64))]
-    );
-    $rpassword=$password;
-  } else {
-    $cpassword=$password;
-    $rpassword='UNIX';
-  }
-
-  if ( $svc_acct->uid  =~ /^(\d+)$/ ) {
+setpriority(0,0,10);
 
-    die "Non-root user ". $svc_acct->username. " has 0 UID!"
-      if $svc_acct->uid == 0 && $svc_acct->username ne 'root';
+print USERS "$radiusprepend\n";
+
+my %usernames;  ## this hack helps keep the passwd files sane
+my @sendmail;
+
+my $svc_domain;
+foreach $svc_domain (sort {$a->domain cmp $b->domain} @svc_domain) {
+
+  my($domain)=$svc_domain->domain;
+  print RCPTHOSTS "$domain\n.$domain\n";
+  print VPOPRCPTHOSTS "$domain\n";
+  print SENDMAIL_CW "$domain\n";
+
+  ###
+  # FORMAT OF THE ASSIGN/USERS FILE HERE
+  print ASSIGN join(":",
+    "+" . $domain . "-",
+    $domain,
+    $vpopuid,
+    $vpopgid,
+    $vpopdir . "/domains/" . $domain,
+    "-",
+    "",
+    "",
+  ), "\n" if $vpopmailmachines[0];
+
+  (mkdir "$spooldir/domains/" . $domain, 0700)
+    or die "Can't create $spooldir/domains/" . $domain .": $!";
+
+  ( open(QMAILDEFAULT,">$spooldir/domains/" . $domain . "/.qmail-default")
+    and flock(QMAILDEFAULT,LOCK_EX|LOCK_NB)  
+  ) or die "Can't open $spooldir/domains/" . $domain . "/.qmail-default: $!";
+
+  ( open(VPASSWD,">$spooldir/domains/" . $domain . "/vpasswd")
+    and flock(VPASSWD,LOCK_EX|LOCK_NB)  
+  ) or die "Can't open $spooldir/domains/" . $domain . "/vpasswd: $!";
+
+  my ($svc_acct);
+
+  if ($svc_domain->getfield('catchall')) {
+    $svc_acct = qsearchs('svc_acct', {'svcnum' => $svc_domain->catchall});
+    die "Cannot find catchall account for domain $domain\n" unless $svc_acct;
+
+    my $username = $svc_acct->username;
+    push @sendmail, "\@$domain\t$username\n";
+    print VIRTUALDOMAINS "$domain:$username-$domain\n",
+                         ".$domain:$username-$domain\n",
+    ;
 
     ###
-    # FORMAT OF FreeBSD MASTER PASSWD FILE HERE
-    print MASTER join(":",
-      $svc_acct->username,             # User name
-      $cpassword,                      # Encrypted password
-      $svc_acct->uid,                  # User ID
-      $svc_acct->gid,                  # Group ID
-      "",                              # Login Class
-      "0",                             # Password Change Time
-      "0",                             # Password Expiration Time
-      $svc_acct->finger,               # Users name
-      $svc_acct->dir,                  # Users home directory
-      $svc_acct->shell,                        # shell
-    ), "\n" ;
+    # FORMAT OF THE .QMAIL-DEFAULT FILE HERE
+    print QMAILDEFAULT "| $vpopdir/bin/vdelivermail \"\" " . $svc_acct->email . "\n"
+      if $vpopmailmachines[0];
 
+  }else{
     ###
-    # FORMAT OF THE PASSWD FILE HERE
-    print PASSWD join(":",
-      $svc_acct->username,
-      'x', # "##". $svc_acct->$username,
-      $svc_acct->uid,
-      $svc_acct->gid,
-      $svc_acct->finger,
-      $svc_acct->dir,
-      $svc_acct->shell,
-    ), "\n";
+    # FORMAT OF THE .QMAIL-DEFAULT FILE HERE
+    print QMAILDEFAULT "| $vpopdir/bin/vdelivermail \"\" bounce-no-mailbox\n"
+      if $vpopmailmachines[0];
+  }
 
-    ###
-    # FORMAT OF THE SHADOW FILE HERE
-    print SHADOW join(":",
-      $svc_acct->username,
-      $cpassword,
-      '',
-      '',
-      '',
-      '',
-      '',
-      '',
-      '',
-    ), "\n";
+  print VPOPVIRTUALDOMAINS "$domain:$domain\n";
+
+  foreach $svc_acct (qsearch('svc_acct', {'domsvc' => $svc_domain->svcnum})) {
+    my($password)=$svc_acct->getfield('_password');
+    my($cpassword,$rpassword);
+    #if ( ( length($password) <= 8 )
+    if ( ( length($password) <= 12 )
+         && ( $password ne '*' )
+         && ( $password ne '!!' )
+         && ( $password ne '' )
+       ) {
+      $cpassword=crypt($password,
+                       $saltset[int(rand(64))].$saltset[int(rand(64))]
+      );
+      $rpassword=$password;
+    } else {
+      $cpassword=$password;
+      $rpassword='UNIX';
+    }
 
-  }
+    my $username;
+
+    if ($mydomain && ($mydomain eq $svc_domain->domain)) {
+      $username=$svc_acct->username;
+    } elsif ($userpolicy =~ /^prepend domsvc$/) {
+      $username=$svc_acct->domsvc . $svc_acct->username;
+    } elsif ($userpolicy =~ /^append domsvc$/) {
+      $username=$svc_acct->username . $svc_acct->domsvc;
+    } elsif ($userpolicy =~ /^append domain$/) {
+      $username=$svc_acct->username . $svc_domain->domain;
+    } elsif ($userpolicy =~ /^append domain$/) {
+      $username=$svc_acct->username . $svc_domain->domain;
+    } elsif ($userpolicy =~ /^append \@domain$/) {
+      $username=$svc_acct->username . '@'. $svc_domain->domain;
+    } else {
+      die "Unknown policy in username_policy\n";
+    }
 
-  if ( $svc_acct->slipip ne '' ) {
+    if ($svc_acct->dir ne '/dev/null' || $svc_acct->slipip ne '') {
+      if ($usernames{$username}++) {
+        die "Duplicate username detected: $username\n";
+      }
+    }
+            
+    if ( $svc_acct->uid  =~ /^(\d+)$/ ) {
+
+      die "Non-root user ". $svc_acct->username. " has 0 UID!"
+        if $svc_acct->uid == 0 && $svc_acct->username ne 'root';
+
+      if ( $svc_acct->dir ne "/dev/null") {
+
+        ###
+        # FORMAT OF FreeBSD MASTER PASSWD FILE HERE
+        print MASTER join(":",
+          $username,                        # User name
+          $cpassword,                       # Encrypted password
+          $svc_acct->uid,                   # User ID
+          $svc_acct->gid,                   # Group ID
+          "",                               # Login Class
+          "0",                              # Password Change Time
+          "0",                              # Password Expiration Time
+          $svc_acct->finger,                # Users name
+          $svc_acct->dir,                   # Users home directory
+          $svc_acct->shell,                 # shell
+        ), "\n" ;
+
+
+        ###
+        # FORMAT OF THE PASSWD FILE HERE
+        print PASSWD join(":",
+          $username,
+          'x', # "##". $username,
+          $svc_acct->uid,
+          $svc_acct->gid,
+          $svc_acct->finger,
+          $svc_acct->dir,
+          $svc_acct->shell,
+        ), "\n";
+
+        ###
+        # FORMAT OF THE SHADOW FILE HERE
+        print SHADOW join(":",
+          $username,
+          $cpassword,
+          '',
+          '',
+          '',
+          '',
+          '',
+          '',
+          '',
+        ), "\n";
+      }
+    }
 
     ###
-    # FORMAT OF THE ACP_* FILES HERE
-    print ACP_PASSWD join(":",
+    # FORMAT OF THE VPASSWD FILE HERE
+    print VPASSWD join(":",
       $svc_acct->username,
       $cpassword,
-      "0",
-      "0",
-      "",
-      "",
-      "",
+      '1',
+      '0',
+      $svc_acct->username,
+      "$vpopdir/domains/" . $svc_domain->domain ."/" . $svc_acct->username,
+      'NOQUOTA',
     ), "\n";
 
-    my($ip)=$svc_acct->slipip;
 
-    unless ( $ip eq '0.0.0.0' || $svc_acct->slipip eq '0e0' ) {
-      print ACP_DIALUP $svc_acct->username, "\t*\t", $svc_acct->slipip, "\n";
-    }
+    if ( $svc_acct->slipip ne '' ) {
+
+      ###
+      # FORMAT OF THE ACP_* FILES HERE
+      print ACP_PASSWD join(":",
+        $username,
+        $cpassword,
+        "0",
+        "0",
+        "",
+        "",
+        "",
+      ), "\n";
+
+      my($ip)=$svc_acct->slipip;
+
+      unless ( $ip eq '0.0.0.0' || $svc_acct->slipip eq '0e0' ) {
+        print ACP_DIALUP $username, "\t*\t", $svc_acct->slipip, "\n";
+      }
+
+      my %radreply = $svc_acct->radius_reply;
+      my %radcheck = $svc_acct->radius_check;
+
+      my $radcheck = join ", ", map { qq($_ = "$radcheck{$_}") } keys %radcheck;
+      $radcheck .= ", " if $radcheck;
+
+      ###
+      # FORMAT OF THE USERS FILE HERE
+      print USERS
+        $username,
+        qq(\t${textradiusprepend}),
+        $radcheck,
+#        qq(Password = "$rpassword"\n\t),
+        join ",\n\t", map { qq($_ = "$radreply{$_}") } keys %radreply;
+
+      #if ( $ip && $ip ne '0e0' ) {
+      #  #print USERS qq(,\n\tFramed-Address = "$ip"\n\n);
+      #  print USERS qq(,\n\tFramed-IP-Address = "$ip"\n\n);
+      #} else {
+        print USERS qq(\n\n);
+      #}
 
+    }
+  
     ###
-    # FORMAT OF THE USERS FILE HERE
-    print USERS
-      $svc_acct->username, qq(\tPassword = "$rpassword"\n\t),
-
-      join ",\n\t",
-        map  {
-          /^(radius_(.*))$/;
-          my($field,$attrib)=($1,$2);
-          $attrib =~ s/_/\-/g;
-          "$attrib = \"". $svc_acct->getfield($field). "\"";
-        } grep /^radius_/ && $svc_acct->getfield($_), fields('svc_acct') 
-    ;
-    if ( $ip && $ip ne '0e0' ) {
-      print USERS qq(,\n\tFramed-Address = "$ip"\n\n);
-    } else {
-      print USERS qq(\n\n);
+    # vpopmail directory structure creation
+
+    (mkdir "$spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username, 0700)
+      or die "Can't create $spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . ": $!";
+    (mkdir "$spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . "/Maildir", 0700)
+      or die "Can't create $spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . " /Maildir: $!";
+    (mkdir "$spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . "/Maildir/cur", 0700)
+      or die "Can't create $spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . " /Maildir/cur: $!";
+    (mkdir "$spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . "/Maildir/new", 0700)
+      or die "Can't create $spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . " /Maildir/new: $!";
+    (mkdir "$spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . "/Maildir/tmp", 0700)
+      or die "Can't create $spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . " /Maildir/tmp: $!";
+
+    ( open(DOTQMAIL,">$spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . "/.qmail")
+      and flock(DOTQMAIL,LOCK_EX|LOCK_NB)  
+    ) or die "Can't open $spooldir/domains/" . $svc_domain->domain . "/" . $svc_acct->username . "/.qmail: $!";
+
+    my($svc_forward);
+    foreach $svc_forward (qsearch('svc_forward', {'srcsvc' => $svc_acct->svcnum})) {
+      my($destination);
+      if ($svc_forward->dstsvc) {
+        my $dst_acct = qsearchs('svc_acct', {'svcnum' => $svc_forward->dstsvc});
+        my $dst_domain = qsearchs('svc_domain', {'svcnum' => $dst_acct->domsvc});
+        $destination = $dst_acct->username . '@' . $dst_domain->domain;
+
+        if ($dst_domain->domain eq $mydomain) {
+          print VIRTUSERTABLE $svc_acct->username . "@" . $svc_domain->domain .
+            "\t" . $dst_acct->username . "\n";
+          print RECIPIENTMAP $svc_acct->username . "@" . $svc_domain->domain .
+            ":$destination\n";
+        }
+      } else {
+        $destination = $svc_forward->dst;
+      }
+    
+      ###
+      # FORMAT OF .QMAIL FILES HERE
+      print DOTQMAIL "$destination\n";
     }
 
+    flock(DOTQMAIL,LOCK_UN);
+    close DOTQMAIL;
+
   }
 
+  flock(VPASSWD,LOCK_UN);
+  flock(QMAILDEFAULT,LOCK_UN);
+  close VPASSWD;
+  close QMAILDEFAULT;
+
 }
 
+###
+# FORMAT OF THE ASSIGN/USERS FILE FINAL LINE HERE
+print ASSIGN ".\n";
+
+print VIRTUSERTABLE @sendmail;
+
 flock(MASTER,LOCK_UN);
 flock(PASSWD,LOCK_UN);
 flock(SHADOW,LOCK_UN);
 flock(ACP_DIALUP,LOCK_UN);
 flock(ACP_PASSWD,LOCK_UN);
 flock(USERS,LOCK_UN);
+flock(ASSIGN,LOCK_UN);
+flock(SENDMAIL_CW,LOCK_UN);
+flock(VIRTUSERTABLE,LOCK_UN);
+flock(RCPTHOSTS,LOCK_UN);
+flock(VPOPRCPTHOSTS,LOCK_UN);
+flock(RECIPIENTMAP,LOCK_UN);
+flock(VPOPVIRTUALDOMAINS,LOCK_UN);
 
 close MASTER;
 close PASSWD;
@@ -270,18 +467,26 @@ close SHADOW;
 close ACP_DIALUP;
 close ACP_PASSWD;
 close USERS;
+close ASSIGN;
+close SENDMAIL_CW;
+close VIRTUSERTABLE;
+close RCPTHOSTS;
+close VPOPRCPTHOSTS;
+close RECIPIENTMAP;
+close VPOPVIRTUALDOMAINS;
 
 ###
 # export stuff
 #
 
-my($shellmachine);
-foreach $shellmachine (@shellmachines) {
-  scp("$spooldir/passwd","root\@$shellmachine:/etc/passwd.new")
-    == 0 or die "scp error: $!";
-  scp("$spooldir/shadow","root\@$shellmachine:/etc/shadow.new")
-    == 0 or die "scp error: $!";
-  ssh("root\@$shellmachine",
+my($ashellmachine);
+foreach $ashellmachine (@shellmachines) {
+  my $scp = new Net::SCP;
+  $scp->scp("$spooldir/passwd","root\@$ashellmachine:/etc/passwd.new")
+    or die "scp error: ". $scp->{errstr};
+  $scp->scp("$spooldir/shadow","root\@$ashellmachine:/etc/shadow.new")
+    or die "scp error: ". $scp->{errstr};
+  ssh("root\@$ashellmachine",
     "( ".
       "mv /etc/passwd.new /etc/passwd; ".
       "mv /etc/shadow.new /etc/shadow; ".
@@ -292,14 +497,16 @@ foreach $shellmachine (@shellmachines) {
 
 my($bsdshellmachine);
 foreach $bsdshellmachine (@bsdshellmachines) {
-  scp("$spooldir/passwd","root\@$bsdshellmachine:/etc/passwd.new")
-    == 0 or die "scp error: $!";
-  scp("$spooldir/master.passwd","root\@$bsdshellmachine:/etc/master.passwd.new")
-    == 0 or die "scp error: $!";
+  my $scp = new Net::SCP;
+  $scp->scp("$spooldir/passwd","root\@$bsdshellmachine:/etc/passwd.new")
+    or die "scp error: ". $scp->{errstr};
+  $scp->scp("$spooldir/master.passwd","root\@$bsdshellmachine:/etc/master.passwd.new")
+    or die "scp error: ". $scp->{errstr};
   ssh("root\@$bsdshellmachine",
     "( ".
       "mv /etc/passwd.new /etc/passwd; ".
-      "mv /etc/master.passwd.new /etc/master.passwd; ".
+      #"mv /etc/master.passwd.new /etc/master.passwd; ".
+      "pwd_mkdb /etc/master.passwd.new; ".
     " )"
   )
     == 0 or die "ssh error: $!";
@@ -307,10 +514,11 @@ foreach $bsdshellmachine (@bsdshellmachines) {
 
 my($nismachine);
 foreach $nismachine (@nismachines) {
-  scp("$spooldir/passwd","root\@$nismachine:/etc/global/passwd")
-    == 0 or die "scp error: $!";
-  scp("$spooldir/shadow","root\@$nismachine:/etc/global/shadow")
-    == 0 or die "scp error: $!";
+  my $scp = new Net::SCP;
+  $scp->scp("$spooldir/passwd","root\@$nismachine:/etc/global/passwd")
+    or die "scp error: ". $scp->{errstr};
+  $scp->scp("$spooldir/shadow","root\@$nismachine:/etc/global/shadow")
+    or die "scp error: ". $scp->{errstr};
   ssh("root\@$nismachine",
     "( ".
       "cd /var/yp; make; ".
@@ -321,10 +529,11 @@ foreach $nismachine (@nismachines) {
 
 my($erpcdmachine);
 foreach $erpcdmachine (@erpcdmachines) {
-  scp("$spooldir/acp_passwd","root\@$erpcdmachine:/usr/annex/acp_passwd")
-    == 0 or die "scp error: $!";
-  scp("$spooldir/acp_dialup","root\@$erpcdmachine:/usr/annex/acp_dialup")
-    == 0 or die "scp error: $!";
+  my $scp = new Net::SCP;
+  $scp->scp("$spooldir/acp_passwd","root\@$erpcdmachine:/usr/annex/acp_passwd")
+    or die "scp error: ". $scp->{errstr};
+  $scp->scp("$spooldir/acp_dialup","root\@$erpcdmachine:/usr/annex/acp_dialup")
+    or die "scp error: ". $scp->{errstr};
   ssh("root\@$erpcdmachine",
     "( ".
       "kill -USR1 \`cat /usr/annex/erpcd.pid\'".
@@ -335,9 +544,10 @@ foreach $erpcdmachine (@erpcdmachines) {
 
 my($radiusmachine);
 foreach $radiusmachine (@radiusmachines) {
-  scp("$spooldir/users","root\@$radiusmachine:/etc/raddb/users")
-    == 0 or die "scp error: $!";
-  ssh("root\@$erpcdmachine",
+  my $scp = new Net::SCP;
+  $scp->scp("$spooldir/users","root\@$radiusmachine:/etc/raddb/users")
+    or die "scp error: ". $scp->{errstr};
+  ssh("root\@$radiusmachine",
     "( ".
       "builddbm".
     " )"
@@ -345,7 +555,87 @@ foreach $radiusmachine (@radiusmachines) {
     == 0 or die "ssh error: $!";
 }
 
+#my @args = ("/bin/tar", "c", "--force-local", "-C", "$spooldir", "-f", "$spooldir/vpoptarball", "domains");
+
+#system {$args[0]} @args;
+
+my($vpopmailmachine);
+foreach $vpopmailmachine (@vpopmailmachines) {
+  my ($machine, $vpopdir, $vpopuid, $vpopgid) = split (/\s+/, $vpopmailmachine);
+  my $scp = new Net::SCP;
+#  $scp->scp("$spooldir/vpoptarball","root\@$machine:vpoptarball")
+#    or die "scp error: ". $scp->{errstr};
+#  ssh("root\@$machine",
+#    "( ".
+#      "rm -rf domains; ".
+#      "tar xf vpoptarball; ".
+#      "chown -R $vpopuid:$vpopgid domains; ".
+#      "tar cf vpoptarball domains; ".
+#      "cd $vpopdir; ".
+#      "tar xf ~/vpoptarball; ".
+#    " )"
+#  )
+#    == 0 or die "ssh error: $!";
+
+  chdir $spooldir;
+  my @args = ("$rsync", "-rlpt", "-e", "$ssh", "domains/", "vpopmail\@$machine:$vpopdir/domains/");
+
+  system {$args[0]} @args;
+
+  $scp->scp("$spooldir/assign","root\@$machine:/var/qmail/users/assign")
+    or die "scp error: ". $scp->{errstr};
+  $scp->scp("$spooldir/vpopvirtualdomains","root\@$machine:/var/qmail/control/virtualdomains")
+    or die "scp error: ". $scp->{errstr};
+  $scp->scp("$spooldir/vpoprcpthosts","root\@$machine:/var/qmail/control/rcpthosts")
+    or die "scp error: ". $scp->{errstr};
+
+  ssh("root\@$machine",
+    "( ".
+      $vpopmailrestart .
+    " )"
+  )
+    == 0 or die "ssh error: $!";
+
+
+}
+
+my($sendmailmachine);
+foreach $sendmailmachine (@sendmailmachines) {
+  my $scp = new Net::SCP;
+  $scp->scp("$spooldir/sendmail.cw","root\@$sendmailmachine:$sendmailconfigpath/sendmail.cw.new")
+    or die "scp error: ". $scp->{errstr};
+  $scp->scp("$spooldir/virtusertable","root\@$sendmailmachine:$sendmailconfigpath/virtusertable.new")
+    or die "scp error: ". $scp->{errstr};
+  ssh("root\@$sendmailmachine",
+    "( ".
+      "mv $sendmailconfigpath/sendmail.cw.new $sendmailconfigpath/sendmail.cw; ".
+      "mv $sendmailconfigpath/virtusertable.new $sendmailconfigpath/virtusertable; ".
+      $sendmailrestart.
+    " )"
+  )
+    == 0 or die "ssh error: $!";
+}
+
+my($qmailmachine);
+foreach $qmailmachine (@qmailmachines) {
+  my $scp = new Net::SCP;
+  $scp->scp("$spooldir/recipientmap","root\@$qmailmachine:/var/qmail/control/recipientmap")
+    or die "scp error: ". $scp->{errstr};
+  $scp->scp("$spooldir/virtualdomains","root\@$qmailmachine:/var/qmail/control/virtualdomains")
+    or die "scp error: ". $scp->{errstr};
+  $scp->scp("$spooldir/rcpthosts","root\@$qmailmachine:/var/qmail/control/rcpthosts")
+    or die "scp error: ". $scp->{errstr};
+  #ssh("root\@$qmailmachine","/etc/init.d/qmail restart")
+  #  == 0 or die "ssh error: $!";
+}
+
 unlink $spoollock;
 flock(EXPORT,LOCK_UN);
 close EXPORT;
 
+#
+
+sub usage {
+  die "Usage:\n\n  svc_acct.export user\n";
+}
+
index c4b8c5e..eb94e1c 100755 (executable)
@@ -1,31 +1,22 @@
 #!/usr/bin/perl -Tw
-#
-# ivan@sisd.com 98-mar-9
-#
-# changed 'password' field to '_password' because PgSQL 6.3 reserves this word
-#      bmccane@maxbaud.net  98-Apr-3
-#
-# generalized svcparts (still needs radius import) ivan@sisd.com 98-mar-23
-#
-# radius import, now an interactive script.  still needs erpcd import?
-# ivan@sisd.com 98-jun-24
-#
-# arbitrary radius attributes ivan@sisd.com 98-aug-9
-#
-# don't import /var/spool/freeside/conf/shells!  ivan@sisd.com 98-aug-13
+# $Id: svc_acct.import,v 1.17 2001-08-19 10:25:44 ivan Exp $
 
 use strict;
 use vars qw(%part_svc);
 use Date::Parse;
-use FS::SSH qw(iscp);
-use FS::UID qw(adminsuidsetup);
+use Term::Query qw(query);
+use Net::SCP qw(iscp);
+use FS::UID qw(adminsuidsetup datasrc);
 use FS::Record qw(qsearch);
 use FS::svc_acct;
+use FS::part_svc;
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
 
-adminsuidsetup;
+push @FS::svc_acct::shells, qw(/bin/sync /sbin/shuddown /bin/halt); #others?
 
-#my($spooldir)="/var/spool/freeside/export";
-my($spooldir)="unix/";
+my($spooldir)="/usr/local/etc/freeside/export.". datasrc;
 
 $FS::svc_acct::nossh_hack = 1;
 
@@ -33,6 +24,8 @@ $FS::svc_acct::nossh_hack = 1;
 
 %part_svc=map { $_->svcpart, $_ } qsearch('part_svc',{'svcdb'=>'svc_acct'});
 
+die "No services with svcdb svc_acct!\n" unless %part_svc;
+
 print "\n\n", &menu_svc, "\n", <<END;
 Most accounts probably have entries in passwd and users (with Port-Limit
 nonexistant or 1).
@@ -58,8 +51,7 @@ my($oisdn_svcpart)=&getpart;
 print "\n\n", &menu_svc, "\n", <<END;
 POP mail accounts have entries in passwd only, and have a particular shell.
 END
-print "Enter that shell: ";
-my($pop_shell)=&getvalue;
+my($pop_shell)=&getvalue("Enter that shell:");
 my($popmail_svcpart)=&getpart;
 
 print "\n\n", &menu_svc, "\n", <<END;
@@ -71,37 +63,38 @@ print "\n\n", <<END;
 Enter the location and name of your _user_ passwd file, for example
 "mail.isp.com:/etc/passwd" or "nis.isp.com:/etc/global/passwd"
 END
-print ":";
-my($loc_passwd)=&getvalue;
+my($loc_passwd)=&getvalue(":");
 iscp("root\@$loc_passwd", "$spooldir/passwd.import");
 
 print "\n\n", <<END;
 Enter the location and name of your _user_ shadow file, for example
 "mail.isp.com:/etc/shadow" or "bsd.isp.com:/etc/master.passwd"
 END
-print ":";
-my($loc_shadow)=&getvalue;
+my($loc_shadow)=&getvalue(":");
 iscp("root\@$loc_shadow", "$spooldir/shadow.import");
 
 print "\n\n", <<END;
 Enter the location and name of your radius "users" file, for example
 "radius.isp.com:/etc/raddb/users"
 END
-print ":";
-my($loc_users)=&getvalue;
+my($loc_users)=&getvalue(":");
 iscp("root\@$loc_users", "$spooldir/users.import");
 
 sub menu_svc {
   ( join "\n", map "$_: ".$part_svc{$_}->svc, sort keys %part_svc ). "\n";
 }
 sub getpart {
-  print "Enter part number, or 0 for none: ";
-  &getvalue;
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query "Enter part number:", 'irk', [ keys %part_svc ];
+  $^W=1;
+  $return;
 }
 sub getvalue {
-  my($x)=scalar(<STDIN>);
-  chop $x;
-  $x;
+  my $prompt = shift;
+  $^W=0; # Term::Query isn't -w-safe
+  my $return = query $prompt, '';
+  $^W=1;
+  $return;
 }
 
 print "\n\n";
@@ -116,12 +109,14 @@ my(%upassword,%ip,%allparam);
 my(%param,$username);
 while (<USERS>) {
   chop;
-  next if /^$/;
+  next if /^\s*$/;
+  next if /^\s*#/;
   if ( /^\S/ ) {
-    /^(\w+)\s+Password\s+=\s+"([^"]+)"(,\s+Expiration\s+=\s+"([^"]*")\s*)?$/
+    /^(\w+)\s+(Auth-Type\s+=\s+Local,\s+)?Password\s+=\s+"([^"]+)"(,\s+Expiration\s+=\s+"([^"]*")\s*)?$/
       or die "1Unexpected line in users.import: $_";
     my($password,$expiration);
-    ($username,$password,$expiration)=(lc($1),$2,$4);
+    ($username,$password,$expiration)=(lc($1),$3,$5);
+    $password = '' if $password eq 'UNIX';
     $upassword{$username}=$password;
     undef %param;
   } else {
@@ -130,8 +125,12 @@ while (<USERS>) {
   while (<USERS>) {
     chop;
     if ( /^\s*$/ ) {
-      $ip{$username}=$param{'radius_Framed_IP_Address'}||'0e0';
-      delete $param{'radius_Framed_IP_Address'};
+      if ( defined $param{'radius_Framed_IP_Address'} ) {
+        $ip{$username} = $param{'radius_Framed_IP_Address'};
+        delete $param{'radius_Framed_IP_Address'};
+      } else {
+        $ip{$username} = '0e0';
+      }
       $allparam{$username}={ %param };
       last;
     } elsif ( /^\s+([\w\-]+)\s=\s"?([\w\.\-\s]+)"?,?\s*$/ ) {
@@ -144,14 +143,20 @@ while (<USERS>) {
   }
 }
 #? incase there isn't a terminating blank line ?
-$ip{$username}=$param{'radius_Framed_IP_Address'}||'0e0';
-delete $param{'radius_Framed_IP_Address'};
+if ( defined $param{'radius_Framed_IP_Address'} ) {
+  $ip{$username} = $param{'radius_Framed_IP_Address'};
+  delete $param{'radius_Framed_IP_Address'};
+} else {
+  $ip{$username} = '0e0';
+}
 $allparam{$username}={ %param };
 
 my(%password);
 while (<SHADOW>) {
   chop;
   my($username,$password)=split(/:/);
+  #$password =~ s/^\!$/\*/;
+  #$password =~ s/\!+/\*SUSPENDED\* /;
   $password{$username}=$password;
 }
 
@@ -176,16 +181,16 @@ while (<PASSWD>) {
     $svcpart = $shell_svcpart;
   }
 
-  my($svc_acct) = create FS::svc_acct ({
-    'svcpart'  => $svcpart,
-    'username' => $username,
-    'password' => $password,
-    'uid'      => $uid,
-    'gid'      => $gid,
-    'finger'   => $finger,
-    'dir'      => $dir,
-    'shell'    => $shell,
-    'slipip'   => $ip{$username},
+  my($svc_acct) = new FS::svc_acct ({
+    'svcpart'   => $svcpart,
+    'username'  => $username,
+    '_password' => $password,
+    'uid'       => $uid,
+    'gid'       => $gid,
+    'finger'    => $finger,
+    'dir'       => $dir,
+    'shell'     => $shell,
+    'slipip'    => $ip{$username},
     %{$allparam{$username}},
   });
   my($error);
@@ -210,11 +215,11 @@ foreach $username ( keys %upassword ) {
     die "Illegal Port-Limit in users!\n";
   }
 
-  my($svc_acct) = create FS::svc_acct ({
-    'svcpart'  => $svcpart,
-    'username' => $username,
-    'password' => $password,
-    'slipip'   => $ip{$username},
+  my($svc_acct) = new FS::svc_acct ({
+    'svcpart'   => $svcpart,
+    'username'  => $username,
+    '_password' => $password,
+    'slipip'    => $ip{$username},
     %{$allparam{$username}},
   });
   my($error);
@@ -225,3 +230,9 @@ foreach $username ( keys %upassword ) {
   delete $upassword{$username};
 }
 
+#
+
+sub usage {
+  die "Usage:\n\n  svc_acct.import user\n";
+}
+
diff --git a/bin/svc_acct_sm.export b/bin/svc_acct_sm.export
deleted file mode 100755 (executable)
index c2ec1e5..0000000
+++ /dev/null
@@ -1,221 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# Create and export VoiceNet_quasar.m4
-#
-# ivan@voicenet.com late oct 96
-#
-# change priority (after copies) to 19, not 10
-# ivan@voicenet.com 97-feb-5
-#
-# put file in different place and run different script, as per matt and
-# mohamed
-# ivan@voicenet.com 97-mar-10
-#
-# added exit if stuff is already locked ivan@voicenet.com 97-apr-15
-#
-# removed mail2
-# ivan@voicenet.com 97-jul-10
-#
-# rewrote lots of the bits, now exports qmail "virtualdomain",
-# "recipientmap" and "rcpthosts" files as well
-#
-# ivan@voicenet.com 97-sep-4
-#
-# adds ".extra" files
-#
-# ivan@voicenet.com 97-sep-29
-#
-# added ".pp" files, ugh.
-#
-# ivan@voicenet.com 97-oct-1
-#
-# rewrite ivan@sisd.com 98-mar-9
-#
-# now can create .qmail-default files ivan@sisd.com 98-mar-10
-#
-# put example $my_domain declaration in ivan@sisd.com 98-mar-23
-#
-# /var/spool/freeside/conf and sendmail updates ivan@sisd.com 98-aug-14
-
-use strict;
-use Fcntl qw(:flock);
-use FS::SSH qw(ssh scp);
-use FS::UID qw(adminsuidsetup);
-use FS::Record qw(qsearch qsearchs);
-
-my($conf_shellm)="/var/spool/freeside/conf/shellmachine";
-my($fqmailmachines)="/var/spool/freeside/conf/qmailmachines";
-my($shellmachine);
-my(@qmailmachines);
-if ( -e $fqmailmachines ) {
-  open(SHELLMACHINE,$conf_shellm) or die "Can't open $conf_shellm: $!";
-  <SHELLMACHINE> =~ /^([\w\.\-]+)$/ or die "Illegal $conf_shellm";
-  $shellmachine = $1;
-  close SHELLMACHINE;
-  open(QMAILMACHINES,$fqmailmachines);
-  @qmailmachines=map {
-    /^(.*)$/ or die "Illegal line in conf/qmailmachines"; #we trust the file
-    $1;
-  } grep $_ !~ /^(#|$)/, <QMAILMACHINES>;
-  close QMAILMACHINES;
-}
-
-my($fsendmailmachines)="/var/spool/freeside/conf/sendmailmachines";
-my(@sendmailmachines);
-if ( -e $fsendmailmachines ) {
-  open(SENDMAILMACHINES,$fsendmailmachines);
-  @sendmailmachines=map {
-    /^(.*)$/ or die "Illegal line in conf/sendmailmachines"; #we trust the file
-    $1;
-  } grep $_ !~ /^(#|$)/, <SENDMAILMACHINES>;
-  close SENDMAILMACHINES;
-}
-
-my($conf_domain)="/var/spool/freeside/conf/domain";
-open(DOMAIN,$conf_domain) or die "Can't open $conf_domain: $!";
-my($mydomain)=map {
-  /^(.*)$/ or die "Illegal line in $conf_domain!"; #yes, we trust the file
-  $1
-} grep $_ !~ /^(#|$)/, <DOMAIN>;
-close DOMAIN;
-
-my($spooldir)="/var/spool/freeside/export";
-my($spoollock)="/var/spool/freeside/svc_acct_sm.export.lock";
-
-adminsuidsetup;
-umask 066;
-
-open(EXPORT,"+>>$spoollock") or die "Can't open $spoollock: $!";
-select(EXPORT); $|=1; select(STDOUT);
-unless ( flock(EXPORT,LOCK_EX|LOCK_NB) ) {
-  seek(EXPORT,0,0);
-  my($pid)=<EXPORT>;
-  chop($pid);
-  #no reason to start locks of blocking processes
-  die "Is another export process running under pid $pid?\n";
-}
-seek(EXPORT,0,0);
-print EXPORT $$,"\n";
-
-my(@svc_acct_sm)=qsearch('svc_acct_sm',{});
-
-( open(RCPTHOSTS,">$spooldir/rcpthosts")
-  and flock(RCPTHOSTS,LOCK_EX|LOCK_NB) 
-) or die "Can't open $spooldir/rcpthosts: $!";
-( open(RECIPIENTMAP,">$spooldir/recipientmap") 
-  and flock(RECIPIENTMAP,LOCK_EX|LOCK_NB) 
-) or die "Can't open $spooldir/recipientmap: $!";
-( open(VIRTUALDOMAINS,">$spooldir/virtualdomains") 
-  and flock(VIRTUALDOMAINS,LOCK_EX|LOCK_NB)
-) or die "Can't open $spooldir/virtualdomains: $!";
-( open(VIRTUSERTABLE,">$spooldir/virtusertable")
-  and flock(VIRTUSERTABLE,LOCK_EX|LOCK_NB)
-) or die "Can't open $spooldir/virtusertable: $!";
-( open(SENDMAIL_CW,">$spooldir/sendmail.cw")
-  and flock(SENDMAIL_CW,LOCK_EX|LOCK_NB)
-) or die "Can't open $spooldir/sendmail.cw: $!";
-
-setpriority(0,0,10);
-
-my($svc_domain,%domain);
-foreach $svc_domain ( qsearch('svc_domain',{}) ) {
-  my($domain)=$svc_domain->domain;
-  $domain{$svc_domain->svcnum}=$domain;
-  print RCPTHOSTS "$domain\n.$domain\n";
-  print SENDMAIL_CW "$domain\n";
-}
-
-my(@sendmail);
-
-my($svc_acct_sm);
-foreach $svc_acct_sm ( qsearch('svc_acct_sm') ) { 
-  my($domsvc,$domuid,$domuser)=(
-    $svc_acct_sm->domsvc,
-    $svc_acct_sm->domuid,
-    $svc_acct_sm->domuser,
-  );
-  my($domain)=$domain{$domsvc};
-  my($svc_acct)=qsearchs('svc_acct',{'uid'=>$domuid});
-  my($username,$dir,$uid,$gid)=(
-    $svc_acct->username,
-    $svc_acct->dir,
-    $svc_acct->uid,
-    $svc_acct->gid,
-  );
-  next unless $username && $domain && $domuser;
-
-  if ($domuser eq '*') {
-    push @sendmail, "\@$domain\t$username\n";
-    print VIRTUALDOMAINS "$domain:$username-$domain\n",
-                         ".$domain:$username-$domain\n",
-    ;
-    ###
-    # qmail
-    ssh("root\@$shellmachine",
-      "[ -e $dir/.qmail-default ] || { touch $dir/.qmail-default; chown $uid:$gid $dir/.qmail-default; }"
-    ) if ( $shellmachine && $dir && $uid );
-
-  } else {
-    print VIRTUSERTABLE "$domuser\@$domain\t$username\n";
-    print RECIPIENTMAP "$domuser\@$domain:$username\@$mydomain\n";
-  }
-
-  print VIRTUSERTABLE @sendmail;
-
-}
-
-chmod 0644, "$spooldir/sendmail.cw",
-            "$spooldir/virtusertable",
-            "$spooldir/rcpthosts",
-            "$spooldir/recipientmap",
-            "$spooldir/virtualdomains",
-;
-
-flock(SENDMAIL_CW,LOCK_UN);
-flock(VIRTUSERTABLE,LOCK_UN);
-flock(RCPTHOSTS,LOCK_UN);
-flock(RECIPIENTMAP,LOCK_UN);
-flock(VIRTUALDOMAINS,LOCK_UN);
-
-close SENDMAIL_CW;
-close VIRTUSERTABLE;
-close RCPTHOSTS;
-close RECIPIENTMAP;
-close VIRTUALDOMAINS;
-
-###
-# export stuff
-#
-
-my($sendmailmachine);
-foreach $sendmailmachine (@sendmailmachines) {
-  scp("$spooldir/sendmail.cw","root\@$sendmailmachine:/etc/sendmail.cw.new")
-    == 0 or die "scp error: $!";
-  scp("$spooldir/virtusertable","root\@$sendmailmachine:/etc/virtusertable.new")
-    == 0 or die "scp error: $!";
-  ssh("root\@$sendmailmachine",
-    "( ".
-      "mv /etc/sendmail.cw.new /etc/sendmail.cw; ".
-      "mv /etc/virtusertable.new /etc/virtusertable; ".
-      #"/etc/init.d/sendmail restart; ".
-    " )"
-  )
-    == 0 or die "ssh error: $!";
-}
-
-my($qmailmachine);
-foreach $qmailmachine (@qmailmachines) {
-  scp("$spooldir/recipientmap","root\@$qmailmachine:/var/qmail/control/recipientmap")
-    == 0 or die "scp error: $!";
-  scp("$spooldir/virtualdomains","root\@$qmailmachine:/var/qmail/control/virtualdomains")
-    == 0 or die "scp error: $!";
-  scp("$spooldir/rcpthosts","root\@$qmailmachine:/var/qmail/control/rcpthosts")
-    == 0 or die "scp error: $!";
-  #ssh("root\@$qmailmachine","/etc/init.d/qmail restart")
-  #  == 0 or die "ssh error: $!";
-}
-
-unlink $spoollock;
-flock(EXPORT,LOCK_UN);
-close EXPORT;
-
diff --git a/bin/svc_acct_sm.import b/bin/svc_acct_sm.import
deleted file mode 100755 (executable)
index 10d7e4c..0000000
+++ /dev/null
@@ -1,252 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# ivan@sisd.com 98-mar-9
-#
-# generalized svcparts ivan@sisd.com 98-mar-23
-
-# You really need to enable ssh into a shell machine as this needs to rename
-# .qmail-extension files.
-#
-# now an interactive script ivan@sisd.com 98-jun-30
-#
-# has an (untested) section for sendmail, s/warn/die/g and generates a program
-# to run on your mail machine _later_ instead of ssh'ing for each user
-# ivan@sisd.com 98-jul-13
-
-use strict;
-use vars qw(%d_part_svc %m_part_svc);
-use FS::SSH qw(iscp);
-use FS::UID qw(adminsuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::svc_acct_sm;
-use FS::svc_domain;
-
-adminsuidsetup;
-
-#my($spooldir)="/var/spool/freeside/export";
-my($spooldir)="unix";
-
-my(%mta) = (
-  1 => "qmail",
-  2 => "sendmail",
-);
-
-###
-
-%d_part_svc =
-  map { $_->svcpart, $_ } qsearch('part_svc',{'svcdb'=>'svc_domain'});
-%m_part_svc =
-  map { $_->svcpart, $_ } qsearch('part_svc',{'svcdb'=>'svc_acct_sm'});
-
-print "\n\n", 
-      ( join "\n", map "$_: ".$d_part_svc{$_}->svc, sort keys %d_part_svc ),
-      "\n\nEnter part number for domains: ";
-my($domain_svcpart)=&getvalue;
-
-print "\n\n", 
-      ( join "\n", map "$_: ".$m_part_svc{$_}->svc, sort keys %m_part_svc ),
-      "\n\nEnter part number for mail aliases: ";
-my($mailalias_svcpart)=&getvalue;
-
-print "\n\n", <<END;
-Select your MTA from the following list.
-END
-print join "\n", map "$_: $mta{$_}", sort keys %mta;
-print "\n\n:";
-my($mta)=&getvalue;
-
-if ( $mta{$mta} eq "qmail" ) {
-
-  print "\n\n", <<END;
-Enter the location and name of your qmail control directory, for example
-"mail.isp.com:/var/qmail/control"
-END
-  print ":";
-  my($control)=&getvalue;
-  iscp("root\@$control/rcpthosts","$spooldir/rcpthosts.import");
-#  iscp("root\@$control/recipientmap","$spooldir/recipientmap.import");
-  iscp("root\@$control/virtualdomains","$spooldir/virtualdomains.import");
-
-#  print "\n\n", <<END;
-#Enter the name of the machine with your user .qmail files, for example
-#"mail.isp.com"
-#END
-#  print ":";
-#  my($shellmachine)=&getvalue;
-
-} elsif ( $mta{$mta} eq "sendmail" ) {
-
-  print "\n\n", <<END;
-Enter the location and name of your sendmail virtual user table, for example
-"mail.isp.com:/etc/virtusertable"
-END
-  print ":";
-  my($virtusertable)=&getvalue;
-  iscp("root\@$virtusertable","$spooldir/virtusertable.import");
-
-  print "\n\n", <<END;
-Enter the location and name of your sendmail.cw file, for example
-"mail.isp.com:/etc/sendmail.cw"
-END
-  print ":";
-  my($sendmail_cw)=&getvalue;
-  iscp("root\@$sendmail_cw","$spooldir/sendmail.cw.import");
-
-} else {
-  die "Unknown MTA!\n";
-}
-
-sub getvalue {
-  my($x)=scalar(<STDIN>);
-  chop $x;
-  $x;
-}
-
-print "\n\n";
-
-###
-
-$FS::svc_domain::whois_hack=1;
-$FS::svc_acct_sm::nossh_hack=1;
-
-if ( $mta{$mta} eq "qmail" ) {
-  open(RCPTHOSTS,"<$spooldir/rcpthosts.import")
-    or die "Can't open $spooldir/rcpthosts.import: $!";
-} elsif ( $mta{$mta} eq "sendmail" ) {
-  open(RCPTHOSTS,"<$spooldir/sendmail.cw.import")
-    or die "Can't open $spooldir/sendmail.cw.import: $!";
-} else {
-  die "Unknown MTA!\n";
-}
-
-my(%svcnum);
-
-while (<RCPTHOSTS>) {
-  next if /^(#|$)/;
-  /^\.?([\w\-\.]+)$/
-    #or do { warn "Strange rcpthosts/sendmail.cw line: $_"; next; };
-    or die "Strange rcpthosts/sendmail.cw line: $_";
-  my $domain = $1;
-  my($svc_domain);
-  unless ( $svc_domain = qsearchs('svc_domain', {'domain'=>$domain} ) ) {
-    $svc_domain = create FS::svc_domain ({
-      'domain'  => $domain,
-      'svcpart' => $domain_svcpart,
-      'action'  => 'N',
-    });
-    my $error = $svc_domain->insert;
-    #warn $error if $error;
-    die $error if $error;
-  }
-  $svcnum{$domain}=$svc_domain->svcnum;
-}
-close RCPTHOSTS; 
-
-#these two loops have enough similar parts they should probably be merged
-if ( $mta{$mta} eq "qmail" ) {
-
-  open(VD_FIX,">$spooldir/virtualdomains.FIX");
-  print VD_FIX "#!/usr/bin/perl\n";
-
-  open(VIRTUALDOMAINS,"<$spooldir/virtualdomains.import")
-    or die "Can't open $spooldir/virtualdomains.import: $!";
-  while (<VIRTUALDOMAINS>) {
-    next if /^#/;
-    /^\.?([\w\-\.]+):(\w+)(\-([\w\-\.]+))?$/
-      #or do { warn "Strange virtualdomains line: $_"; next; };
-      or die "Strange virtualdomains line: $_";
-    my($domain,$username,$dash_ext,$extension)=($1,$2,$3,$4);
-    $dash_ext ||= '';
-    $extension ||= '';
-    my($svc_acct)=qsearchs('svc_acct',{'username'=>$username});
-    unless ( $svc_acct ) {
-      #warn "Unknown user $username in virtualdomains; skipping\n";
-      #die "Unknown user $username in virtualdomains; skipping\n";
-      next;
-    }
-    if ( $domain ne $extension ) {
-      #warn "virtualdomains line $domain:$username$dash_ext changed to $domain:$username-$domain\n";
-      my($dir)=$svc_acct->dir;
-      my($qdomain)=$domain;
-      $qdomain =~ s/\./:/g; #see manpage for 'dot-qmail': EXTENSION ADDRESSES
-      #example to move .qmail files for virtual domains to their new location 
-      #dry run
-      #issh("root\@$shellmachine",'perl -e \'foreach $a (<'. $dir. '/.qmail'. $dash_ext. '-*>) { $old=$a; $a =~ s/\\.qmail'. $dash_ext. '\\-/\\.qmail\\-'. $qdomain. '\\-/; print " $old -> $a\n"; }\'');
-      #the real thing
-      #issh("root\@$shellmachine",'perl -e \'foreach $a (<'. $dir. '/.qmail'. $dash_ext. '-*>) { $old=$a; $a =~ s/\\.qmail'. $dash_ext. '\\-/\\.qmail\\-'. $qdomain. '\\-/; rename $old, $a; }\'');
-      print VD_FIX <<END;
-foreach \$file (<$dir/.qmail$dash_ext-*>) {
-  \$old = \$file;
-  \$file =~ s/\.qmail$dash_ext\-/\.qmail\-$qdomain\-/;
-  rename \$old, \$file;
-}
-END
-    }
-
-    unless ( exists $svcnum{$domain} ) {
-      my($svc_domain) = create FS::svc_domain ({
-        'domain'  => $domain,
-        'svcpart' => $domain_svcpart,
-        'action'  => 'N',
-      });
-      my $error = $svc_domain->insert;
-      #warn $error if $error;
-      die $error if $error;
-      $svcnum{$domain}=$svc_domain->svcnum;
-    }
-
-    my($svc_acct_sm)=create FS::svc_acct_sm ({
-      'domsvc'  => $svcnum{$domain},
-      'domuid'  => $svc_acct->uid,
-      'domuser' => '*',
-      'svcpart' => $mailalias_svcpart,
-    });
-    my($error)='';
-    $error=$svc_acct_sm->insert;
-    #warn $error if $error;
-    die $error, ", domain $domain" if $error;
-  }
-  close VIRTUALDOMAINS;
-  close VD_FIX;
-
-} elsif ( $mta{$mta} eq "sendmail" ) {
-
-  open(VIRTUSERTABLE,"<$spooldir/virtusertable.import")
-    or die "Can't open $spooldir/virtusertable.import: $!";
-  while (<VIRTUSERTABLE>) {
-    next if /^#/; #comments?
-    /^([\w\-\.]+)?\@([\w\-\.]+)\t([\w\-\.]+)$/
-      #or do { warn "Strange virtusertable line: $_"; next; };
-      or die "Strange virtusertable line: $_";
-    my($domuser,$domain,$username)=($1,$2,$3);
-    my($svc_acct)=qsearchs('svc_acct',{'username'=>$username});
-    unless ( $svc_acct ) {
-      #warn "Unknown user $username in virtusertable";
-      die "Unknown user $username in virtusertable";
-      next;
-    }
-    my($svc_acct_sm)=create FS::svc_acct_sm ({
-      'domsvc'  => $svcnum{$domain},
-      'domuid'  => $svc_acct->uid,
-      'domuser' => $domuser || '*',
-      'svcpart' => $mailalias_svcpart,
-    });
-    my($error)='';
-    $error=$svc_acct_sm->insert;
-    #warn $error if $error;
-    die $error if $error;
-  }
-  close VIRTUSERTABLE;
-
-} else {
-  die "Unknown MTA!\n";
-}
-
-#open(RECIPIENTMAP,"<$spooldir/recipientmap.import");
-#close RECIPIENTMAP;
-
-print "\n\n", <<END if $mta{$mta} eq "qmail";
-Don\'t forget to run $spooldir/virtualdomains.FIX before using
-$spooldir/virtualdomains !
-END
-
diff --git a/bin/svc_domain.erase b/bin/svc_domain.erase
new file mode 100755 (executable)
index 0000000..c023661
--- /dev/null
@@ -0,0 +1,17 @@
+#!/usr/bin/perl -w
+#
+# $Id: svc_domain.erase,v 1.1 2002-04-20 11:57:35 ivan Exp $
+
+use strict;
+use FS::UID qw(adminsuidsetup);
+use FS::Record qw(qsearch);
+
+use FS::domain_record;
+use FS::svc_domain;
+
+adminsuidsetup(shift @ARGV) or die "Usage: svc_domain.erase user\n";
+
+foreach my $record ( qsearch('domain_record',{}), qsearch('svc_domain', {} ) ) {
+  my $error = $record->delete;
+  die $error if $error;
+}
diff --git a/bin/sysvshell.export b/bin/sysvshell.export
new file mode 100755 (executable)
index 0000000..c13912c
--- /dev/null
@@ -0,0 +1,112 @@
+#!/usr/bin/perl -w
+
+# sysvshell export
+
+use strict;
+use File::Rsync;
+use Net::SSH qw(ssh);
+use FS::UID qw(adminsuidsetup datasrc);
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export;
+use FS::cust_svc;
+use FS::svc_acct;
+
+my @saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+
+my $user = shift or die &usage;
+adminsuidsetup $user;
+
+my $spooldir = "/usr/local/etc/freeside/export.". datasrc;
+#my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/shell";
+
+my @sysv_exports = qsearch('part_export', { 'exporttype' => 'sysvshell' } );
+
+my $rsync = File::Rsync->new({
+  rsh     => 'ssh',
+#  dry_run => 1,
+});
+
+foreach my $export ( @sysv_exports ) {
+  my $machine = $export->machine;
+  my $prefix = "$spooldir/$machine";
+  mkdir $prefix, 0700 unless -d $prefix;
+
+  #LOCKING!!!
+
+  ( open(SHADOW,">$prefix/shadow")
+    #!!!  and flock(SHADOW,LOCK_EX|LOCK_NB)
+  ) or die "Can't open $prefix/shadow: $!";
+  ( open(PASSWD,">$prefix/passwd")
+    #!!!  and flock(PASSWD,LOCK_EX|LOCK_NB)
+  ) or die "Can't open $prefix/passwd: $!";
+
+  chmod 0644, "$prefix/passwd";
+  chmod 0600, "$prefix/shadow";
+
+  my @svc_acct = $export->svc_x;
+
+  next unless @svc_acct;
+
+  foreach my $svc_acct ( sort { $a->uid <=> $b->uid } @svc_acct ) {
+
+    my $password = $svc_acct->_password;
+    my $cpassword;
+    #if ( ( length($password) <= 8 )
+    if ( ( length($password) <= 12 )
+         && ( $password ne '*' )
+         && ( $password ne '!!' )
+         && ( $password ne '' )
+    ) {
+      $cpassword=crypt($password,
+                       $saltset[int(rand(64))].$saltset[int(rand(64))]
+      );
+      # MD5 !!!!
+    } else {
+      $cpassword=$password;
+    }
+
+    ###
+    # FORMAT OF THE PASSWD FILE HERE
+    print PASSWD join(":",
+      $svc_acct->username,
+      'x', # "##". $username,
+      $svc_acct->uid,
+      $svc_acct->gid,
+      $svc_acct->finger,
+      $svc_acct->dir,
+      $svc_acct->shell,
+    ), "\n";
+
+    ###
+    # FORMAT OF THE SHADOW FILE HERE
+    print SHADOW join(":",
+      $svc_acct->username,
+      $cpassword,
+      '',
+      '',
+      '',
+      '',
+      '',
+      '',
+      '',
+    ), "\n";
+
+  }
+
+  #!!! flock(SHADOW,LOCK_UN);
+  #!!! flock(PASSWD,LOCK_UN);
+  close SHADOW;
+  close PASSWD;
+
+  $rsync->exec( {
+    src  => "$prefix/shadow",
+    dest => "root\@$machine:/etc/shadow"
+  } ) or die "rsync to $machine failed: ". join(" / ", $rsync->err);
+
+  $rsync->exec( {
+    src  => "$prefix/passwd",
+    dest => "root\@$machine:/etc/passwd"
+  } ) or die "rsync to $machine failed: ". join(" / ", $rsync->err);
+
+  # UNLOCK!!
+}
diff --git a/conf/address b/conf/address
deleted file mode 100644 (file)
index b8b6610..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-Silicon Interactive Software Design
-119 Signal Hill Road
-Holland, PA  18966-2924
-
diff --git a/conf/agent_defaultpkg b/conf/agent_defaultpkg
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/conf/alerter_template b/conf/alerter_template
new file mode 100644 (file)
index 0000000..4d8a012
--- /dev/null
@@ -0,0 +1,20 @@
+
+
+Ivan Kohler
+1339 Hayes St.
+San Francisco, CA  94117
+
+
+{ $first; } { $last; }:
+
+  We thank you for your continuing patronage.  This notice is to remind you
+that your { $payby } used to pay SISD.COM for Internet
+service will expire on { use Date::Format; time2str("%x", $expdate); }.  Please provide us with new billing
+information so that we may continue your service uninterrupted.
+
+Very Truly Yours,
+
+  SISD Service Team
+
+
+
diff --git a/conf/declinetemplate b/conf/declinetemplate
new file mode 100644 (file)
index 0000000..14b8c60
--- /dev/null
@@ -0,0 +1,10 @@
+Hi,
+
+Your credit card could not be processed for the following reason:
+  { $error }
+
+Please provide us with new billing information so that we may continue your
+service uninterrupted.
+
+Thanks.
+
diff --git a/conf/domain b/conf/domain
deleted file mode 100644 (file)
index b3cefaf..0000000
+++ /dev/null
@@ -1 +0,0 @@
-domain.tld
diff --git a/conf/invoice_from b/conf/invoice_from
new file mode 100644 (file)
index 0000000..110ec8f
--- /dev/null
@@ -0,0 +1 @@
+ivan-unconfigured-freeside-installation@420.am
diff --git a/conf/invoice_template b/conf/invoice_template
new file mode 100644 (file)
index 0000000..e226d63
--- /dev/null
@@ -0,0 +1,27 @@
+
+                                 Invoice
+                                 { substr("Page $page of $total_pages          ", 0, 19); } { use Date::Format; time2str("%x", $date); }  FS-{ $invnum; }
+
+
+Ivan Kohler
+1339 Hayes St.
+San Francisco, CA  94117
+
+
+{ $address[0]; }
+{ $address[1]; }
+{ $address[2]; }
+{ $address[3]; }
+{ $address[4]; }
+{ $address[5]; }
+
+{
+  join("\n",
+    map {
+      my ( $desc, $price ) = @{$_};
+      "  ". substr( $desc. " "x65, 0, 65). " ". substr( $price. " "x11, 0, 11);
+    } invoice_lines(31)
+  );
+}
+
+ -=> Freeside - open-source billing for ISPs - http://www.sisd.com/freeside <=-
diff --git a/conf/locale b/conf/locale
new file mode 100644 (file)
index 0000000..7741b83
--- /dev/null
@@ -0,0 +1 @@
+en_US
diff --git a/conf/maxsearchrecordsperpage b/conf/maxsearchrecordsperpage
new file mode 100644 (file)
index 0000000..29d6383
--- /dev/null
@@ -0,0 +1 @@
+100
diff --git a/conf/registries/internic/from b/conf/registries/internic/from
deleted file mode 100644 (file)
index dc36ae7..0000000
+++ /dev/null
@@ -1 +0,0 @@
-domreg@domain.tld
diff --git a/conf/registries/internic/nameservers b/conf/registries/internic/nameservers
deleted file mode 100644 (file)
index e1aa999..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-192.168.1.1     ns1.domain.tld
-192.168.1.2     ns2.domain.tld
-192.168.1.3     ns3.domain.tld
diff --git a/conf/registries/internic/tech_contact b/conf/registries/internic/tech_contact
deleted file mode 100644 (file)
index 1e6fea0..0000000
+++ /dev/null
@@ -1 +0,0 @@
-A1
diff --git a/conf/registries/internic/template b/conf/registries/internic/template
deleted file mode 100644 (file)
index 8e4983c..0000000
+++ /dev/null
@@ -1,231 +0,0 @@
-[ URL ftp://rs.internic.net/templates/domain-template.txt ] [ 03/98 ] 
-
-******* Please DO NOT REMOVE Version Number or Sections A-Q ********
-
-Domain Version Number: 4.0
-
-******* Email completed agreement to hostmaster@internic.net *******
-
-       NETWORK SOLUTIONS, INC.
-
-       DOMAIN NAME REGISTRATION AGREEMENT
-
-
-A.     Introduction. This domain name registration agreement
-("Registration Agreement") is submitted to NETWORK SOLUTIONS, INC.
-("NSI") for the purpose of applying for and registering a domain name
-on the Internet. If this Registration Agreement is accepted by NSI,
-and a domain name is registered in NSI's domain name database and
-assigned to the Registrant, Registrant ("Registrant") agrees to be
-bound by the terms of this Registration Agreement and the terms of
-NSI's Domain Name Dispute Policy ("Dispute Policy") which is
-incorporated herein by reference and made a part of this Registration
-Agreement. This Registration Agreement shall be accepted at the
-offices of NSI. 
-
-B. Fees and Payments.
-
-1) Registration or renewal (re-registration) date through March 31, 1998:
-Registrant agrees to pay a registration fee of One Hundred United States
-Dollars (US$100) as consideration for the registration of each new domain
-name or Fifty United States Dollars (US$50) to renew (re-register) an
-existing registration.
-2) Registration or renewal date on and after April 1, 1998:  Registrant
-agrees to pay a registration fee of Seventy United States Dollars (US$70) 
-as consideration for the registration of each new domain name or the 
-applicable renewal (re-registration) fee (currently Thirty-Five United 
-States Dollars (US$35)) at the time of renewal (re-registration).
-3) Period of Service:  The non-refundable fee covers a period of two (2)
-years for each new registration, and one (1) year for each renewal, 
-and includes any permitted modification(s) to the domain name record
-during the covered period.
-4) Payment:  Payment is due to Network Solutions within thirty (30) 
-days from the date of the invoice.
-
-C.     Dispute Policy. Registrant agrees, as a condition to
-submitting this Registration Agreement, and if the Registration
-Agreement is accepted by NSI, that the Registrant shall be bound by
-NSI's current Dispute Policy. The current version of the Dispute
-Policy may be found at the InterNIC Registration Services web site:
-"http://www.netsol.com/rs/dispute-policy.html". 
-
-D.     Dispute Policy Changes or Modifications. Registrant agrees
-that NSI, in its sole discretion, may change or modify the Dispute
-Policy, incorporated by reference herein, at any time. Registrant
-agrees that Registrant's maintaining the registration of a domain name
-after changes or modifications to the Dispute Policy become effective
-constitutes Registrant's continued acceptance of these changes or
-modifications. Registrant agrees that if Registrant considers any such
-changes or modifications to be unacceptable, Registrant may request
-that the domain name be deleted from the domain name database. 
-
-E.     Disputes. Registrant agrees that, if the registration of its
-domain name is challenged by any third party, the Registrant will be
-subject to the provisions specified in the Dispute Policy. 
-
-F.     Agents. Registrant agrees that if this Registration Agreement
-is completed by an agent for the Registrant, such as an ISP or
-Administrative Contact/Agent, the Registrant is nonetheless bound as a
-principal by all terms and conditions herein, including the Dispute
-Policy. 
-
-G.     Limitation of Liability. Registrant agrees that NSI shall have
-no liability to the Registrant for any loss Registrant may incur in
-connection with NSI's processing of this Registration Agreement, in
-connection with NSI's processing of any authorized modification to the
-domain name's record during the covered period, as a result of the
-Registrant's ISP's failure to pay either the initial registration fee
-or renewal fee, or as a result of the application of the provisions of
-the Dispute Policy. Registrant agrees that in no event shall the
-maximum liability of NSI under this Agreement for any matter exceed
-Five Hundred United States Dollars (US$500). 
-
-H.     Indemnity. Registrant agrees, in the event the Registration
-Agreement is accepted by NSI and a subsequent dispute arises with any
-third party, to indemnify and hold NSI harmless pursuant to the terms
-and conditions contained in the Dispute Policy. 
-
-I.     Breach. Registrant agrees that failure to abide by any
-provision of this Registration Agreement or the Dispute Policy may be
-considered by NSI to be a material breach and that NSI may provide a
-written notice, describing the breach, to the Registrant. If, within
-thirty (30) days of the date of mailing such notice, the Registrant
-fails to provide evidence, which is reasonably satisfactory to NSI,
-that it has not breached its obligations, then NSI may delete
-Registrant's registration of the domain name. Any such breach by a
-Registrant shall not be deemed to be excused simply because NSI did
-not act earlier in response to that, or any other, breach by the
-Registrant. 
-
-J.     No Guaranty. Registrant agrees that, by registration of a
-domain name, such registration does not confer immunity from objection
-to either the registration or use of the domain name. 
-
-K.     Warranty. Registrant warrants by submitting this Registration
-Agreement that, to the best of Registrant's knowledge and belief, the
-information submitted herein is true and correct, and that any future
-changes to this information will be provided to NSI in a timely manner
-according to the domain name modification procedures in place at that
-time. Breach of this warranty will constitute a material breach. 
-
-L.     Revocation. Registrant agrees that NSI may delete a
-Registrant's domain name if this Registration Agreement, or subsequent
-modification(s) thereto, contains false or misleading information, or
-conceals or omits any information NSI would likely consider material
-to its decision to approve this Registration Agreement. 
-
-M.     Right of Refusal. NSI, in its sole discretion, reserves the
-right to refuse to approve the Registration Agreement for any
-Registrant. Registrant agrees that the submission of this Registration
-Agreement does not obligate NSI to accept this Registration Agreement.
-Registrant agrees that NSI shall not be liable for loss or damages
-that may result from NSI's refusal to accept this Registration
-Agreement. 
-
-N.     Severability. Registrant agrees that the terms of this
-Registration Agreement are severable. If any term or provision is
-declared invalid, it shall not affect the remaining terms or
-provisions which shall continue to be binding. 
-
-O.     Entirety. Registrant agrees that this Registration Agreement
-and the Dispute Policy is the complete and exclusive agreement between
-Registrant and NSI regarding the registration of Registrant's domain
-name. This Registration Agreement and the Dispute Policy supersede all
-prior agreements and understandings, whether established by custom,
-practice, policy, or precedent. 
-
-P.     Governing Law. Registrant agrees that this Registration
-Agreement shall be governed in all respects by and construed in
-accordance with the laws of the Commonwealth of Virginia, United
-States of America. By submitting this Registration Agreement,
-Registrant consents to the exclusive jurisdiction and venue of the
-United States District Court for the Eastern District of Virginia,
-Alexandria Division. If there is no jurisdiction in the United States
-District Court for the Eastern District of Virginia, Alexandria
-Division, then jurisdiction shall be in the Circuit Court of Fairfax
-County, Fairfax, Virginia. 
-
-Q.     This is Domain Name Registration Agreement Version
-Number 4.0. This Registration Agreement is only for registrations
-under top-level domains: COM, ORG, NET, and EDU. By completing
-and submitting this Registration Agreement for consideration and
-acceptance by NSI, the Registrant agrees that he/she has read and
-agrees to be bound by A through P above. 
-
-
-Authorization
-0a.  (N)ew (M)odify (D)elete....:###action###
-0b.  Auth Scheme................: 
-0c.  Auth Info..................: 
-
-1.   Comments...................:###purpose###
-
-2.   Complete Domain Name.......:###domain###
-
-Organization Using Domain Name
-
-3a.  Organization Name..........:###company###
-###LOOP###
-3b.  Street Address.............:###address###
-###ENDLOOP###
-3c.  City.......................:###city###
-3d.  State......................:###state###
-3e.  Postal Code................:###zip###
-3f.  Country....................:###country###
-
-Administrative Contact
-4a.  NIC Handle (if known)......: 
-4b.  (I)ndividual (R)ole........:I
-4c.  Name (Last, First).........:###last###, ###first###
-4d.  Organization Name..........:###company###
-###LOOP###
-4e.  Street Address.............:###address###
-###ENDLOOP###
-4f.  City.......................:###city###
-4g.  State......................:###state###
-4h.  Postal Code................:###zip### 
-4i.  Country....................:###country###
-4j.  Phone Number...............:###daytime###
-4k.  Fax Number.................:###fax###
-4l.  E-Mailbox..................:###email###
-
-Technical Contact
-5a.  NIC Handle (if known)......:###tech_contact###
-5b.  (I)ndividual (R)ole........: 
-5c.  Name (Last, First).........: 
-5d.  Organization Name..........: 
-5e.  Street Address.............: 
-5f.  City.......................: 
-5g.  State......................: 
-5h.  Postal Code................: 
-5i.  Country....................: 
-5j.  Phone Number...............: 
-5k.  Fax Number.................: 
-5l.  E-Mailbox..................: 
-
-Billing Contact
-6a.  NIC Handle (if known)......: 
-6b.  (I)ndividual (R)ole........: 
-6c.  Name (Last, First).........: 
-6d.  Organization Name..........: 
-6e.  Street Address.............: 
-6f.  City.......................: 
-6g.  State......................:
-6h.  Postal Code................:
-6i.  Country....................:
-6j.  Phone Number...............:
-6k.  Fax Number.................: 
-6l.  E-Mailbox..................: 
-
-Prime Name Server
-7a.  Primary Server Hostname....:###primary###
-7b.  Primary Server Netaddress..:###primary_ip###
-
-Secondary Name Server(s)
-###LOOP###
-8a.  Secondary Server Hostname..:###secondary###
-8b.  Secondary Server Netaddress:###secondary_ip###
-###ENDLOOP###
-
-END OF AGREEMENT
-
diff --git a/conf/registries/internic/to b/conf/registries/internic/to
deleted file mode 100644 (file)
index c80f93c..0000000
+++ /dev/null
@@ -1 +0,0 @@
-hostmaster@internic.net
diff --git a/conf/report_template b/conf/report_template
new file mode 100644 (file)
index 0000000..9c6bb2b
--- /dev/null
@@ -0,0 +1,14 @@
+{ sprintf("%-19s", "Page $page of $total_pages"); } { 
+ my $spacer = (40 - length($title) > 0) ? 40 - length($title) : 0;
+ $spacer = int($spacer / 2);
+ my $titlelen = 40 - $spacer;
+ sprintf("%*s%-*s", $spacer, " ", $titlelen, $title);
+ } { use Date::Format; time2str("%x %X", $date); } 
+
+
+{
+  join("\n", map { $_ } report_lines(57));
+}
+
+
+
diff --git a/conf/secrets b/conf/secrets
deleted file mode 100644 (file)
index 5843943..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-DBI:mysql:freeside
-freeside
-put_your_password_here
index 02d74f7..a41fc62 100644 (file)
@@ -1,2 +1,5 @@
-/bin/csh
+
 /bin/sh
+/bin/csh
+/bin/bash
+/bin/false
diff --git a/conf/show-msgcat-codes b/conf/show-msgcat-codes
new file mode 100644 (file)
index 0000000..e69de29
index fa7963c..2fbb50c 100644 (file)
@@ -1 +1 @@
-mail
+localhost
diff --git a/conf/soadefaultttl b/conf/soadefaultttl
new file mode 100644 (file)
index 0000000..92f616f
--- /dev/null
@@ -0,0 +1 @@
+259200
diff --git a/conf/soaexpire b/conf/soaexpire
new file mode 100644 (file)
index 0000000..d235b91
--- /dev/null
@@ -0,0 +1 @@
+3600000
diff --git a/conf/soarefresh b/conf/soarefresh
new file mode 100644 (file)
index 0000000..9f35f8e
--- /dev/null
@@ -0,0 +1 @@
+10800
diff --git a/conf/soaretry b/conf/soaretry
new file mode 100644 (file)
index 0000000..bb08106
--- /dev/null
@@ -0,0 +1 @@
+1800
diff --git a/debian/README.Debian b/debian/README.Debian
new file mode 100644 (file)
index 0000000..b51eee8
--- /dev/null
@@ -0,0 +1,6 @@
+freeside for Debian
+-------------------
+
+<possible notes regarding this package - if none, delete this file>
+
+ -- Ivan Kohler <ivan-debian@420.am>, Thu, 12 Apr 2001 15:49:17 -0700
diff --git a/debian/changelog b/debian/changelog
new file mode 100644 (file)
index 0000000..d8283b5
--- /dev/null
@@ -0,0 +1,9 @@
+freeside (1.4.1-1) unstable; urgency=low
+
+  * Initial Release.
+
+ -- Ivan Kohler <ivan-debian@420.am>  Thu, 12 Apr 2001 15:49:17 -0700
+
+Local variables:
+mode: debian-changelog
+End:
diff --git a/debian/conffiles.ex b/debian/conffiles.ex
new file mode 100644 (file)
index 0000000..8686d2a
--- /dev/null
@@ -0,0 +1,7 @@
+#
+# If you want to use this conffile, remove all comments and put files that
+# you want dpkg to process here using their absolute pathnames.
+# See section 9.1 of the packaging manual.
+#
+# for example:
+# /etc/freeside/freeside.conf
diff --git a/debian/control b/debian/control
new file mode 100644 (file)
index 0000000..de2820d
--- /dev/null
@@ -0,0 +1,92 @@
+Source: freeside
+Section: admin
+Priority: optional
+Maintainer: Ivan Kohler <ivan-debian@420.am>
+Build-Depends: debhelper (>> 3.0.0)
+Standards-Version: 3.5.2
+
+Package: freeside
+Architecture: any
+Depends: freeside-lib
+Recommends: freeside-doc, freeside-ui-web, libterm-query-perl
+Suggests: freeside-passwd-server, freeside-signup-server, freeside-session-server, freeside-selfservice-server
+Description: Billing and administration package for ISPs.
+ Freeside is a billing and account administration package for ISPs.  It stores
+ customer information in an SQL database, and will update UNIX passwd and
+ shadow files, RADIUS users file and SQL databases, and configuration for
+ sendmail, qmail, BIND and/or Apache.  It is also useful as a central database
+ of accounts/domains/web-space for a large number of machines.
+
+Package: freeside-doc
+Architecture: all
+Description: Documentation for freeside
+ This package provides the HTML documentation for Freeside, a billing and
+ account administration package for ISPs.
+
+Package: freeside-lib
+Architecture: all
+Depends: libmime-base64-perl, libdigest-md5-perl, liburi-perl, libhtml-tagset-perl, libhtml-parser-perl, libnet-perl, liblocale-codes-perl, libnet-whois-perl, libwww-perl, libbusiness-creditcard-perl, libmailtools-perl, libtimedate-perl, libdate-manip-perl, libfile-counterfile-perl, libfreezethaw-perl, libtext-template-perl, libdbd-pg-perl, libdbix-datasource-perl, libdbix-dbschema-perl, libnet-ssh-perl, libnet-scp-perl, libapache-asp-perl, libtie-ixhash-perl, libtime-duration-perl, libhtml-widgets-selectlayers-perl, libstorable-perl, libapache-dbi-perl
+Description: Freeside libraries and extension API
+ This package contains the libraries which implement the business logic and
+ backend functions of Freeside, a billing and account administration package
+ for ISPs.  This package also contains the manual pages for the library API.
+
+Package: freeside-ui-web
+Architecture: all
+Depends: libstring-approx-perl, freeside-lib, libapache-mod-perl|apache-perl
+Suggests: libapache-mod-ssl|apache-ssl
+Description: Easy-to-use web interface for Freeside
+ This package contains the web interface for Freeside, a billing and account
+ administration package for ISPs.  This is what sales or support folks will
+ typically use to add new accounts, edit exiting accounts and so on.
+
+Package: freeside-passwd-server
+Architecture: all
+Depends: freeside-lib
+Description: Freeside password server 
+ This component of Freeside, a billing and account administration package for
+ ISPs, 
+
+Package: freeside-passwd-client
+Architecture: all
+Depends: 
+Description: 
+ <rar>
+
+Package: freeside-signup-server
+Architecture: all
+Depends: freeside-lib
+Description:
+ <rar>
+
+Package: freeside-signup-client
+Architecture: all
+Depends: 
+Description:
+ <rar>
+
+Package: freeside-signup-client-webui
+Architecture: all
+Depends: freeside-signup-client-lib, httpd
+Description: 
+ <rar>
+
+Package: freeside-session-server
+Architecture: all
+Depends: freeside-lib
+Description:
+ <rar>
+
+Package: freeside-session-client
+Architecture: all
+Depends: ssh
+Description: 
+ <rar>
+
+Package: freeside-selfservice-server
+Architecture: all
+Depends:
+Description:
+ <rar>
+
+
diff --git a/debian/copyright b/debian/copyright
new file mode 100644 (file)
index 0000000..e148fce
--- /dev/null
@@ -0,0 +1,10 @@
+This package was debianized by Ivan Kohler <ivan-debian@420.am> on
+Thu, 12 Apr 2001 15:49:17 -0700.
+
+It was downloaded from <fill in ftp site>
+
+Upstream Author(s): <put author(s) name and email here>
+
+Copyright:
+
+<Must follow here>
diff --git a/debian/cron.d.ex b/debian/cron.d.ex
new file mode 100644 (file)
index 0000000..61c074d
--- /dev/null
@@ -0,0 +1,4 @@
+#
+# Regular cron jobs for the freeside package
+#
+0 4    * * *   root    freeside_maintenance
diff --git a/debian/dirs b/debian/dirs
new file mode 100644 (file)
index 0000000..ca882bb
--- /dev/null
@@ -0,0 +1,2 @@
+usr/bin
+usr/sbin
diff --git a/debian/docs b/debian/docs
new file mode 100644 (file)
index 0000000..16636bd
--- /dev/null
@@ -0,0 +1,3 @@
+INSTALL
+README
+TODO
diff --git a/debian/ex.doc-base.package b/debian/ex.doc-base.package
new file mode 100644 (file)
index 0000000..2a055d1
--- /dev/null
@@ -0,0 +1,22 @@
+Document: freeside
+Title: Debian freeside Manual
+Author: <insert document author here>
+Abstract: This manual describes what freeside is
+ and how it can be used to
+ manage online manuals on Debian systems.
+Section: unknown
+
+Format: debiandoc-sgml
+Files: /usr/share/doc/freeside/freeside.sgml.gz
+
+Format: postscript
+Files: /usr/share/doc/freeside/freeside.ps.gz
+
+Format: text
+Files: /usr/share/doc/freeside/freeside.text.gz
+
+Format: HTML
+Index: /usr/share/doc/freeside/html/index.html
+Files: /usr/share/doc/freeside/html/*.html
+
+  
diff --git a/debian/freeside-doc.docs b/debian/freeside-doc.docs
new file mode 100644 (file)
index 0000000..299950c
--- /dev/null
@@ -0,0 +1,2 @@
+#DOCS#
+
diff --git a/debian/freeside-doc.files b/debian/freeside-doc.files
new file mode 100644 (file)
index 0000000..299950c
--- /dev/null
@@ -0,0 +1,2 @@
+#DOCS#
+
diff --git a/debian/init.d.ex b/debian/init.d.ex
new file mode 100644 (file)
index 0000000..5791049
--- /dev/null
@@ -0,0 +1,70 @@
+#! /bin/sh
+#
+# skeleton     example file to build /etc/init.d/ scripts.
+#              This file should be used to construct scripts for /etc/init.d.
+#
+#              Written by Miquel van Smoorenburg <miquels@cistron.nl>.
+#              Modified for Debian GNU/Linux
+#              by Ian Murdock <imurdock@gnu.ai.mit.edu>.
+#
+# Version:     @(#)skeleton  1.8  03-Mar-1998  miquels@cistron.nl
+#
+# This file was automatically customized by dh-make on Thu, 12 Apr 2001 15:49:17 -0700
+
+PATH=/sbin:/bin:/usr/sbin:/usr/bin
+DAEMON=/usr/sbin/freeside
+NAME=freeside
+DESC=freeside
+
+test -f $DAEMON || exit 0
+
+set -e
+
+case "$1" in
+  start)
+       echo -n "Starting $DESC: "
+       start-stop-daemon --start --quiet --pidfile /var/run/$NAME.pid \
+               --exec $DAEMON
+       echo "$NAME."
+       ;;
+  stop)
+       echo -n "Stopping $DESC: "
+       start-stop-daemon --stop --quiet --pidfile /var/run/$NAME.pid \
+               --exec $DAEMON
+       echo "$NAME."
+       ;;
+  #reload)
+       #
+       #       If the daemon can reload its config files on the fly
+       #       for example by sending it SIGHUP, do it here.
+       #
+       #       If the daemon responds to changes in its config file
+       #       directly anyway, make this a do-nothing entry.
+       #
+       # echo "Reloading $DESC configuration files."
+       # start-stop-daemon --stop --signal 1 --quiet --pidfile \
+       #       /var/run/$NAME.pid --exec $DAEMON
+  #;;
+  restart|force-reload)
+       #
+       #       If the "reload" option is implemented, move the "force-reload"
+       #       option to the "reload" entry above. If not, "force-reload" is
+       #       just the same as "restart".
+       #
+       echo -n "Restarting $DESC: "
+       start-stop-daemon --stop --quiet --pidfile \
+               /var/run/$NAME.pid --exec $DAEMON
+       sleep 1
+       start-stop-daemon --start --quiet --pidfile \
+               /var/run/$NAME.pid --exec $DAEMON
+       echo "$NAME."
+       ;;
+  *)
+       N=/etc/init.d/$NAME
+       # echo "Usage: $N {start|stop|restart|reload|force-reload}" >&2
+       echo "Usage: $N {start|stop|restart|force-reload}" >&2
+       exit 1
+       ;;
+esac
+
+exit 0
diff --git a/debian/manpage.1.ex b/debian/manpage.1.ex
new file mode 100644 (file)
index 0000000..ec542bb
--- /dev/null
@@ -0,0 +1,60 @@
+.\"                                      Hey, EMACS: -*- nroff -*-
+.\" First parameter, NAME, should be all caps
+.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
+.\" other parameters are allowed: see man(7), man(1)
+.TH FREESIDE SECTION "April 12, 2001"
+.\" Please adjust this date whenever revising the manpage.
+.\"
+.\" Some roff macros, for reference:
+.\" .nh        disable hyphenation
+.\" .hy        enable hyphenation
+.\" .ad l      left justify
+.\" .ad b      justify to both left and right margins
+.\" .nf        disable filling
+.\" .fi        enable filling
+.\" .br        insert line break
+.\" .sp <n>    insert n+1 empty lines
+.\" for manpage-specific macros, see man(7)
+.SH NAME
+freeside \- program to do something
+.SH SYNOPSIS
+.B freeside
+.RI [ options ] " files" ...
+.br
+.B bar
+.RI [ options ] " files" ...
+.SH DESCRIPTION
+This manual page documents briefly the
+.B freeside
+and
+.B bar
+commands.
+This manual page was written for the Debian GNU/Linux distribution
+because the original program does not have a manual page.
+Instead, it has documentation in the GNU Info format; see below.
+.PP
+.\" TeX users may be more comfortable with the \fB<whatever>\fP and
+.\" \fI<whatever>\fP escape sequences to invode bold face and italics, 
+.\" respectively.
+\fBfreeside\fP is a program that...
+.SH OPTIONS
+These programs follow the usual GNU command line syntax, with long
+options starting with two dashes (`-').
+A summary of options is included below.
+For a complete description, see the Info files.
+.TP
+.B \-h, \-\-help
+Show summary of options.
+.TP
+.B \-v, \-\-version
+Show version of program.
+.SH SEE ALSO
+.BR bar (1),
+.BR baz (1).
+.br
+The programs are documented fully by
+.IR "The Rise and Fall of a Fooish Bar" ,
+available via the Info system.
+.SH AUTHOR
+This manual page was written by Ivan Kohler <ivan-debian@420.am>,
+for the Debian GNU/Linux system (but may be used by others).
diff --git a/debian/manpage.sgml.ex b/debian/manpage.sgml.ex
new file mode 100644 (file)
index 0000000..9bc3a86
--- /dev/null
@@ -0,0 +1,143 @@
+<!doctype refentry PUBLIC "-//OASIS//DTD DocBook V4.1//EN" [
+
+<!-- Process this file with docbook-to-man to generate an nroff manual
+     page: `docbook-to-man manpage.sgml > manpage.1'.  You may view
+     the manual page with: `docbook-to-man manpage.sgml | nroff -man |
+     less'.  A typical entry in a Makefile or Makefile.am is:
+
+manpage.1: manpage.sgml
+       docbook-to-man $< > $@
+  -->
+
+  <!-- Fill in your name for FIRSTNAME and SURNAME. -->
+  <!ENTITY dhfirstname "<firstname>FIRSTNAME</firstname>">
+  <!ENTITY dhsurname   "<surname>SURNAME</surname>">
+  <!-- Please adjust the date whenever revising the manpage. -->
+  <!ENTITY dhdate      "<date>April 12, 2001</date>">
+  <!-- SECTION should be 1-8, maybe w/ subsection other parameters are
+       allowed: see man(7), man(1). -->
+  <!ENTITY dhsection   "<manvolnum>SECTION</manvolnum>">
+  <!ENTITY dhemail     "<email>ivan-debian@420.am</email>">
+  <!ENTITY dhusername  "Ivan Kohler">
+  <!ENTITY dhucpackage "<refentrytitle>FREESIDE</refentrytitle>">
+  <!ENTITY dhpackage   "freeside">
+
+  <!ENTITY debian      "<productname>Debian GNU/Linux</productname>">
+  <!ENTITY gnu         "<acronym>GNU</acronym>">
+]>
+
+<refentry>
+  <refentryinfo>
+    <address>
+      &dhemail;
+    </address>
+    <author>
+      &dhfirstname;
+      &dhsurname;
+    </author>
+    <copyright>
+      <year>2001</year>
+      <holder>&dhusername;</holder>
+    </copyright>
+    &dhdate;
+  </refentryinfo>
+  <refmeta>
+    &dhucpackage;
+
+    &dhsection;
+  </refmeta>
+  <refnamediv>
+    <refname>&dhpackage;</refname>
+
+    <refpurpose>program to do something</refpurpose>
+  </refnamediv>
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>&dhpackage;</command>
+
+      <arg><option>-e <replaceable>this</replaceable></option></arg>
+
+      <arg><option>--example <replaceable>that</replaceable></option></arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+  <refsect1>
+    <title>DESCRIPTION</title>
+
+    <para>This manual page documents briefly the
+      <command>&dhpackage;</command> and <command>bar</command>
+      commands.</para>
+
+    <para>This manual page was written for the &debian; distribution
+      because the original program does not have a manual page.
+      Instead, it has documentation in the &gnu;
+      <application>Info</application> format; see below.</para>
+
+    <para><command>&dhpackage;</command> is a program that...</para>
+
+  </refsect1>
+  <refsect1>
+    <title>OPTIONS</title>
+
+    <para>These programs follow the usual GNU command line syntax,
+      with long options starting with two dashes (`-').  A summary of
+      options is included below.  For a complete description, see the
+      <application>Info</application> files.</para>
+
+    <variablelist>
+      <varlistentry>
+        <term><option>-h</option>
+          <option>--help</option>
+        </term>
+        <listitem>
+          <para>Show summary of options.</para>
+        </listitem>
+      </varlistentry>
+      <varlistentry>
+        <term><option>-v</option>
+          <option>--version</option>
+        </term>
+        <listitem>
+          <para>Show version of program.</para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+  <refsect1>
+    <title>SEE ALSO</title>
+
+    <para>bar (1), baz (1).</para>
+
+    <para>The programs are documented fully by <citetitle>The Rise and
+      Fall of a Fooish Bar</citetitle> available via the
+      <application>Info</application> system.</para>
+  </refsect1>
+  <refsect1>
+    <title>AUTHOR</title>
+
+    <para>This manual page was written by &dhusername; &dhemail; for
+      the &debian; system (but may be used by others).  Permission is
+      granted to copy, distribute and/or modify this document under
+      the terms of the <acronym>GNU</acronym> Free Documentation
+      License, Version 1.1 or any later version published by the Free
+      Software Foundation; with no Invariant Sections, no Front-Cover
+      Texts and no Back-Cover Texts.</para>
+
+  </refsect1>
+</refentry>
+
+<!-- Keep this comment at the end of the file
+Local variables:
+mode: sgml
+sgml-omittag:t
+sgml-shorttag:t
+sgml-minimize-attributes:nil
+sgml-always-quote-attributes:t
+sgml-indent-step:2
+sgml-indent-data:t
+sgml-parent-document:nil
+sgml-default-dtd-file:nil
+sgml-exposed-tags:nil
+sgml-local-catalogs:nil
+sgml-local-ecat-files:nil
+End:
+-->
diff --git a/debian/menu.ex b/debian/menu.ex
new file mode 100644 (file)
index 0000000..ddc947e
--- /dev/null
@@ -0,0 +1,2 @@
+?package(freeside):needs=X11|text|vc|wm section=Apps/see-menu-manual\
+  title="freeside" command="/usr/bin/freeside"
diff --git a/debian/postinst.ex b/debian/postinst.ex
new file mode 100644 (file)
index 0000000..c4d4bfb
--- /dev/null
@@ -0,0 +1,47 @@
+#! /bin/sh
+# postinst script for freeside
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+#        * <postinst> `configure' <most-recently-configured-version>
+#        * <old-postinst> `abort-upgrade' <new version>
+#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>
+#          <new-version>
+#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
+#          <failed-install-package> <version> `removing'
+#          <conflicting-package> <version>
+# for details, see /usr/share/doc/packaging-manual/
+#
+# quoting from the policy:
+#     Any necessary prompting should almost always be confined to the
+#     post-installation script, and should be protected with a conditional
+#     so that unnecessary prompting doesn't happen if a package's
+#     installation fails and the `postinst' is called with `abort-upgrade',
+#     `abort-remove' or `abort-deconfigure'.
+
+case "$1" in
+    configure)
+
+    ;;
+
+    abort-upgrade|abort-remove|abort-deconfigure)
+
+    ;;
+
+    *)
+        echo "postinst called with unknown argument \`$1'" >&2
+        exit 0
+    ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
+
+
diff --git a/debian/postrm.ex b/debian/postrm.ex
new file mode 100644 (file)
index 0000000..bed8abd
--- /dev/null
@@ -0,0 +1,36 @@
+#! /bin/sh
+# postrm script for freeside
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+#        * <postrm> `remove'
+#        * <postrm> `purge'
+#        * <old-postrm> `upgrade' <new-version>
+#        * <new-postrm> `failed-upgrade' <old-version>
+#        * <new-postrm> `abort-install'
+#        * <new-postrm> `abort-install' <old-version>
+#        * <new-postrm> `abort-upgrade' <old-version>
+#        * <disappearer's-postrm> `disappear' <r>overwrit>r> <new-version>
+# for details, see /usr/share/doc/packaging-manual/
+
+case "$1" in
+       purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
+
+
+        ;;
+
+    *)
+        echo "postrm called with unknown argument \`$1'" >&2
+        exit 0
+
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+
diff --git a/debian/preinst.ex b/debian/preinst.ex
new file mode 100644 (file)
index 0000000..0b42bb2
--- /dev/null
@@ -0,0 +1,42 @@
+#! /bin/sh
+# preinst script for freeside
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+#        * <new-preinst> `install'
+#        * <new-preinst> `install' <old-version>
+#        * <new-preinst> `upgrade' <old-version>
+#        * <old-preinst> `abort-upgrade' <new-version>
+#
+# For details see /usr/share/doc/packaging-manual/
+
+case "$1" in
+    install|upgrade)
+#        if [ "$1" = "upgrade" ]
+#        then
+#            start-stop-daemon --stop --quiet --oknodo  \
+#                --pidfile /var/run/freeside.pid  \
+#                --exec /usr/sbin/freeside 2>/dev/null || true
+#        fi
+    ;;
+
+    abort-upgrade)
+    ;;
+
+    *)
+        echo "preinst called with unknown argument \`$1'" >&2
+        exit 0
+    ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
+
+
diff --git a/debian/prerm.ex b/debian/prerm.ex
new file mode 100644 (file)
index 0000000..ebb87c5
--- /dev/null
@@ -0,0 +1,37 @@
+#! /bin/sh
+# prerm script for freeside
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+#        * <prerm> `remove'
+#        * <old-prerm> `upgrade' <new-version>
+#        * <new-prerm> `failed-upgrade' <old-version>
+#        * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
+#        * <deconfigured's-prerm> `deconfigure' `in-favour'
+#          <package-being-installed> <version> `removing'
+#          <conflicting-package> <version>
+# for details, see /usr/share/doc/packaging-manual/
+
+case "$1" in
+    remove|upgrade|deconfigure)
+#       install-info --quiet --remove /usr/info/freeside.info.gz
+        ;;
+    failed-upgrade)
+        ;;
+    *)
+        echo "prerm called with unknown argument \`$1'" >&2
+        exit 0
+    ;;
+esac
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
+
+exit 0
+
+
diff --git a/debian/rules b/debian/rules
new file mode 100755 (executable)
index 0000000..71016c4
--- /dev/null
@@ -0,0 +1,113 @@
+#!/usr/bin/make -f
+# Sample debian/rules that uses debhelper. 
+# GNU copyright 1997 by Joey Hess.
+#
+# This version is for a hypothetical package that builds an
+# architecture-dependant package, as well as an architecture-independent
+# package.
+
+# Uncomment this to turn on verbose mode. 
+#export DH_VERBOSE=1
+
+# This is the debhelper compatability version to use.
+export DH_COMPAT=3
+
+configure: configure-stamp
+configure-stamp:
+       dh_testdir
+       # Add here commands to configure the package.
+       
+
+       touch configure-stamp
+
+build: configure-stamp build-stamp
+build-stamp:
+       dh_testdir
+
+       # Add here commands to compile the package.
+       $(MAKE)
+
+       touch build-stamp
+
+clean:
+       dh_testdir
+       dh_testroot
+       rm -f build-stamp configure-stamp
+
+       # Add here commands to clean up after the build process.
+       -$(MAKE) clean
+
+       dh_clean
+
+install: DH_OPTIONS=
+install: build
+       dh_testdir
+       dh_testroot
+       dh_clean -k
+       dh_installdirs
+
+       # Add here commands to install the package into debian/freeside.
+       $(MAKE) install DESTDIR=$(CURDIR)/debian/freeside
+
+       dh_movefiles
+
+# Build architecture-independent files here.
+# Pass -i to all debhelper commands in this target to reduce clutter.
+binary-indep: build install
+       dh_testdir -i
+       dh_testroot -i
+#      dh_installdebconf -i
+       dh_installdocs -i
+       dh_installexamples -i
+       dh_installmenu -i
+#      dh_installlogrotate -i
+#      dh_installemacsen -i
+#      dh_installpam -i
+#      dh_installmime -i
+#      dh_installinit -i
+       dh_installcron -i
+#      dh_installman -i
+       dh_installinfo -i
+#      dh_undocumented -i
+       dh_installchangelogs  -i
+       dh_link -i
+       dh_compress -i
+       dh_fixperms -i
+       dh_installdeb -i
+#      dh_perl -i
+       dh_gencontrol -i
+       dh_md5sums -i
+       dh_builddeb -i
+
+# Build architecture-dependent files here.
+binary-arch: build install
+       dh_testdir -a
+       dh_testroot -a
+#      dh_installdebconf -a
+       dh_installdocs -a
+       dh_installexamples -a
+       dh_installmenu -a
+#      dh_installlogrotate -a
+#      dh_installemacsen -a
+#      dh_installpam -a
+#      dh_installmime -a
+#      dh_installinit -a
+       dh_installcron -a
+#      dh_installman -a
+       dh_installinfo -a
+#      dh_undocumented -a
+       dh_installchangelogs  -a
+       dh_strip -a
+       dh_link -a
+       dh_compress -a
+       dh_fixperms -a
+#      dh_makeshlibs -a
+       dh_installdeb -a
+#      dh_perl -a
+       dh_shlibdeps -a
+       dh_gencontrol -a
+       dh_md5sums -a
+       dh_builddeb -a
+
+binary: binary-indep binary-arch
+.PHONY: build clean binary-indep binary-arch binary install configure
diff --git a/debian/watch.ex b/debian/watch.ex
new file mode 100644 (file)
index 0000000..3f57ae0
--- /dev/null
@@ -0,0 +1,5 @@
+# Example watch control file for uscan
+# Rename this file to "watch" and then you can run the "uscan" command
+# to check for upstream updates and more.
+# Site         Directory               Pattern                 Version Script
+sunsite.unc.edu        /pub/Linux/Incoming     freeside-(.*)\.tar\.gz  debian  uupdate
index 39a5785..e91a2f1 100755 (executable)
@@ -1,17 +1,21 @@
 #!/usr/bin/perl -w
-
+#
 # Template for importing legacy customer data
 #
-# ivan@sisd.com 98-aug-17 - 20
+# $Id: TEMPLATE_cust_main.import,v 1.4 2001-08-21 02:44:47 ivan Exp $
 
 use strict;
+use Date::Parse;
 use FS::UID qw(adminsuidsetup datasrc);
 use FS::Record qw(fields qsearch qsearchs);
 use FS::cust_main;
 use FS::cust_pkg;
-use Date::Parse;
+use FS::cust_svc;
+use FS::svc_acct;
+use FS::pkg_svc;
 
-adminsuidsetup;
+my $user = shift or die &usage;
+adminsuidsetup $user;
 
 # use these for the imported cust_main records (unless you have these in legacy
 # data)
@@ -90,7 +94,7 @@ while (<CLIENT>) {
   $svc{'First'} =~ s/&/and/go; 
   $svc{'Zip'} =~ s/\s+$//go;
 
-  my($cust_main) = create FS::cust_main ( {
+  my($cust_main) = new FS::cust_main ( {
     'custnum'  => $svc{'custnum'},
     'agentnum' => $agentnum,
     'last'     => $svc{'last'},
@@ -121,7 +125,7 @@ while (<CLIENT>) {
     die $error;
   }
 
-  my($cust_pkg)=create FS::cust_pkg ( {
+  my($cust_pkg)=new FS::cust_pkg ( {
     'custnum' => $svc{'custnum'},
     'pkgpart' => $pkgpart{$svc{'LegacyBillingData'}},
     'setup'   => '', 
@@ -168,7 +172,7 @@ while (<CLIENT>) {
         } else {
 
           #create new cust_svc record linked to cust_pkg record 
-          my($n_cust_svc) = create FS::cust_svc ({
+          my($n_cust_svc) = new FS::cust_svc ({
             'svcnum'  => $o_cust_svc->svcnum,
             'pkgnum'  => $cust_pkg->pkgnum,
             'svcpart' => $pkg_svc->svcpart,
@@ -187,3 +191,8 @@ while (<CLIENT>) {
 
 warn "\n$link of $line lines linked\n";
 
+# ---
+
+sub usage {
+  die "Usage:\n\n  cust_main.import user\n";
+}
diff --git a/eg/export_template.pm b/eg/export_template.pm
new file mode 100644 (file)
index 0000000..ca58d4b
--- /dev/null
@@ -0,0 +1,56 @@
+package FS::part_export::myexport;
+
+use vars qw(@ISA);
+use FS::part_export;
+
+@ISA = qw(FS::part_export);
+
+sub rebless { shift; }
+
+sub _export_insert {
+  my($self, $svc_something) = (shift, shift);
+  $err_or_queue = $self->myexport_queue( $svc_something->svcnum, 'insert',
+    $svc_something->username, $svc_something->_password );
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub _export_replace {
+  my( $self, $new, $old ) = (shift, shift, shift);
+  #return "can't change username with myexport"
+  #  if $old->username ne $new->username;
+  #return '' unless $old->_password ne $new->_password;
+  $err_or_queue = $self->myexport_queue( $new->svcnum,
+    'replace', $new->username, $new->_password );
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub _export_delete {
+  my( $self, $svc_something ) = (shift, shift);
+  $err_or_queue = $self->myexport_queue( $svc_something->svcnum,
+    'delete', $svc_something->username );
+  ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+#a good idea to queue anything that could fail or take any time
+sub myexport_queue {
+  my( $self, $svcnum, $method ) = (shift, shift, shift);
+  my $queue = new FS::queue {
+    'svcnum' => $svcnum,
+    'job'    => "FS::part_export::myexport::myexport_$method",
+  };
+  $queue->insert( @_ ) or $queue;
+}
+
+sub myexport_insert { #subroutine, not method
+  my( $username, $password ) = @_;
+  #do things with $username and $password
+}
+
+sub myexport_replace { #subroutine, not method
+}
+
+sub myexport_delete { #subroutine, not method
+  my( $username ) = @_;
+  #do things with $username
+}
+
diff --git a/eg/table_template-svc.pm b/eg/table_template-svc.pm
new file mode 100644 (file)
index 0000000..ebf7299
--- /dev/null
@@ -0,0 +1,161 @@
+package FS::svc_table;
+
+use strict;
+use vars qw(@ISA);
+#use FS::Record qw( qsearch qsearchs );
+use FS::svc_Common;
+use FS::cust_svc;
+
+@ISA = qw(svc_Common);
+
+=head1 NAME
+
+FS::table_name - Object methods for table_name records
+
+=head1 SYNOPSIS
+
+  use FS::table_name;
+
+  $record = new FS::table_name \%hash;
+  $record = new FS::table_name { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->suspend;
+
+  $error = $record->unsuspend;
+
+  $error = $record->cancel;
+
+=head1 DESCRIPTION
+
+An FS::table_name object represents an example.  FS::table_name inherits from
+FS::svc_Common.  The following fields are currently supported:
+
+=over 4
+
+=item field - description
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'table_name'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
+defined.  An FS::cust_svc record will be created and inserted.
+
+=cut
+
+sub insert {
+  my $self = shift;
+  my $error;
+
+  $error = $self->SUPER::insert;
+  return $error if $error;
+
+  '';
+}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+  my $self = shift;
+  my $error;
+
+  $error = $self->SUPER::delete;
+  return $error if $error;
+
+  '';
+}
+
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+sub replace {
+  my ( $new, $old ) = ( shift, shift );
+  my $error;
+
+  $error = $new->SUPER::replace($old);
+  return $error if $error;
+
+  '';
+}
+
+=item suspend
+
+Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item unsuspend
+
+Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item cancel
+
+Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and repalce methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $x = $self->setfixed;
+  return $x unless ref($x);
+  my $part_svc = $x;
+
+
+  ''; #no error
+}
+
+=back
+
+=head1 BUGS
+
+The author forgot to customize this manpage.
+
+=head1 SEE ALSO
+
+L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
+L<FS::cust_pkg>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/eg/table_template.pm b/eg/table_template.pm
new file mode 100644 (file)
index 0000000..d609bd5
--- /dev/null
@@ -0,0 +1,112 @@
+package FS::table_name;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::table_name - Object methods for table_name records
+
+=head1 SYNOPSIS
+
+  use FS::table_name;
+
+  $record = new FS::table_name \%hash;
+  $record = new FS::table_name { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::table_name object represents an example.  FS::table_name inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item field - description
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'table_name'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  ''; #no error
+}
+
+=back
+
+=head1 BUGS
+
+The author forgot to customize this manpage.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/etc/abbr_state.txt b/etc/abbr_state.txt
new file mode 100644 (file)
index 0000000..7e4f57f
--- /dev/null
@@ -0,0 +1,72 @@
+State/Possession               Abbreviation
+
+ALABAMA                         AL
+ALASKA                          AK
+AMERICAN SAMOA                  AS
+ARIZONA                         AZ
+ARKANSAS                        AR
+CALIFORNIA                      CA
+COLORADO                        CO
+CONNECTICUT                     CT
+DELAWARE                        DE
+DISTRICT OF COLUMBIA            DC
+FEDERATED STATES OF MICRONESIA  FM
+FLORIDA                         FL
+GEORGIA                         GA
+GUAM                            GU
+HAWAII                          HI
+IDAHO                           ID
+ILLINOIS                        IL
+INDIANA                         IN
+IOWA                            IA
+KANSAS                          KS
+KENTUCKY                        KY
+LOUISIANA                       LA
+MAINE                           ME
+MARSHALL ISLANDS                MH
+MARYLAND                        MD
+MASSACHUSETTS                   MA
+MICHIGAN                        MI
+MINNESOTA                       MN
+MISSISSIPPI                     MS
+MISSOURI                        MO
+MONTANA                         MT
+NEBRASKA                        NE
+NEVADA                          NV
+NEW HAMPSHIRE                   NH
+NEW JERSEY                      NJ
+NEW MEXICO                      NM
+NEW YORK                        NY
+NORTH CAROLINA                  NC
+NORTH DAKOTA                    ND
+NORTHERN MARIANA ISLANDS        MP
+OHIO                            OH
+OKLAHOMA                        OK
+OREGON                          OR
+PALAU                           PW
+PENNSYLVANIA                    PA
+PUERTO RICO                     PR
+RHODE ISLAND                    RI
+SOUTH CAROLINA                  SC
+SOUTH DAKOTA                    SD
+TENNESSEE                       TN
+TEXAS                           TX
+UTAH                            UT
+VERMONT                         VT
+VIRGIN ISLANDS                  VI
+VIRGINIA                        VA
+WASHINGTON                      WA
+WEST VIRGINIA                   WV
+WISCONSIN                       WI
+WYOMING                         WY
+
+
+Military "State"               Abbreviation
+
+Armed Forces Africa            AE
+Armed Forces Americas          AA
+(except Canada)
+Armed Forces Canada            AE
+Armed Forces Europe            AE
+Armed Forces Middle East       AE
+Armed Forces Pacific           AP
diff --git a/etc/acp_logfile-parse b/etc/acp_logfile-parse
deleted file mode 100755 (executable)
index 5e25899..0000000
+++ /dev/null
@@ -1,197 +0,0 @@
-#!/usr/bin/perl
-
-###
-# WHO WROTE THIS???
-###
-
-#require "perldb.pl";
-
-#    Compute SLIP/PPP log times
-#     Arguments    -a   Process entire file with totals
-#                  -t   Process only totals
-#                  -f   File to be processed if not current
-#                  -d   processing start date (default is entire file)
-#                  -l   to return all totals for dayuse
-#                  -w   name of tmp work file for dayuse
-#                  user names
-
-require "time.pl";
-
-$space='        ';
-
-unless (@ARGV[0]) {
-       print "Missing Arguments\n";
-       print    "-a - entire file\n";
-       print    "-t - totals only\n";
-       print    "-f - file name to be processed\n";
-       print    "-d - processing start date (yymmdd)\n";
-       print    "-l - return totals for dayuse\n";
-       print    "-w - tmp work file for dayuse\n";
-       exit;
-}     # end if test for missing arguments
-
-$infile = "/usr/annex/acp_logfile";
-$tmpfile = "/tmp/ppp";
-$n = $#ARGV;
-$start_yymmdd = "";
-for ($i = 0; $i <= $n; $i++) {
-    if ($ARGV[$i] eq "-a") {
-             $allflag = "true";
-        }
-       elsif ($ARGV[$i] eq "-t") {
-                      $totalflag = "true";
-             }
-       elsif ($ARGV[$i] eq "-f") {
-        $i++;
-          $infile = $ARGV[$i];
-             }
-        elsif ($ARGV[$i] eq "-d") {
-          $i++;
-          $start_yymmdd = $ARGV[$i];
-          }   #end start yymmdd
-        elsif ($ARGV[$i] eq "-l") {
-           $logflag = "true";
-           $totalflag = "true";
-         }  #  end log 
-       elsif ($ARGV[$i] eq "-w") {
-        $i++;
-          $tmpfile = $ARGV[$i];
-             } #  end tmp file 
-        else    {
-            ($arg_user,$arg_yymmdd) = split (/:/, $ARGV[$i]);
-                 $ip_user_date {$arg_user} = $ARGV[$i];
-             $userflag = "true";
-                }   # end else
- } # end for 1 = 1 to n
-
-open (IN,$infile)
-        || die "Can't open acp_logfile";
-
-NEXTUSER: while (<IN>) {        
-        chop;
-        ($add,$ether,$port,$date,$time,$type,$action,$user) = split(/:/);
-
-        if ($logflag) {
-          $start_yymmdd = '';
-          if ($ip_user_date{$user}) {
-             ($ip_user, $start_yymmdd) = 
-                      split (/:/, $ip_user_date{$user});
-           }  # end get date
-        }   #  end log flag
-        if ($start_yymmdd) {
-           if ($date < $start_yymmdd) {
-               next NEXTUSER;
-           }  #end date compare
-        }  #end if date
-        if ($userflag){
-          if (!$ip_user_date{$user}) {
-               next NEXTUSER;
-          }  #  end user test
-        }  #   end by user or all
-        if (($totalflag) ||
-           ($allflag) ||
-           ($ip_user_date{$user})) {
-         if (($type eq 'ppp') || ($type eq 'slip'))  {
-
-            if ($action eq 'login') {
-                        $login{$user} = "$time:$date";
-
-                }
-                  elsif ($action eq 'logout') {
-                     if (!$login{$user}) {
-                          $login{$user} = "010101:$date";
-                      } #end pad user if carry over
-                        ($stime,$sdate) = split(':',$login{$user});
-                        $start = &annex2sec($stime);
-                        $end = &annex2sec($time);
-                        
-                        #If we went through midnight, add a day;
-                        if ($end < $start) {$end += 86400;}
-                        $timeon = $end - $start;
-
-                        $elapsed{$user} += $timeon;
-                        
-                      if (!$totalflag) {
-                        print (&fmt_user($user),
-                              '  ', &fmt_date($sdate), '  In: ', 
-                                &fmt_time($stime),'  Out: ',
-                                &fmt_time($time),
-                       '  Elapsed: ', &fmt_sec($timeon), "\n");
-                      }  # end total test
-                }  #end elsif action
-        }  #  type = ppp of slip
-    }  #  check arguments
-} 
-close IN;
-
-if ($logflag) {
-    open (TMPPPP, ">$tmpfile")
-               || die "Can't open ppp tmp file";
-    foreach $user ( sort((keys(%elapsed))) ) {
-        $log_time = &fmt_sec($elapsed{$user});
-        $tmp = join (':',
-                    $user, 
-                    $log_time);
-        print (TMPPPP "$tmp\n");
-    }
-    close (TMPPPP);
-}
-    else {
-        print "\n\nTotal Time On For Period:\n";
-        print     "-------------------------\n";
-
-        foreach $user ( sort((keys(%elapsed))) ) {
-           print (&fmt_user($user), "  ",&fmt_sec($elapsed{$user}), "\n");
-        }
-    }
-exit(0);
-
-#-------------------------------------------------------
-#--------------- Subroutines Start Here ----------------
-#-------------------------------------------------------
-
-sub annex2sec {
-        local($time) = @_;
-        return( &time2sec( &break_annex($time) ) );
-}
-
-sub fmt_date {
-        local($date) = @_;
-
-        return( substr($date,2,2).'/'.substr($date,4,2).'/'.substr($date,0,2) );
-}
-
-sub fmt_time {
-        local($time) = @_;
-        local($s,$m,$h) = &break_annex($time);
-        return ("$h:$m:$s");
-}
-
-
-sub break_annex {
-        local($time) = @_;
-        local($h,$m,$s);
-
-        $h=substr($time,0,2);
-        $m=substr($time,2,2);
-        $s=substr($time,4,2);
-
-        return ($s,$m,$h);
-}       
-
-sub fmt_sec {
-        local(@t) = &sec2time(@_);
-        @t[2] += (@t[3]*24);
-
-        foreach $a (@t) {
-                if ($a < 10) {$a = "0$a";}
-        }
-
-        return ("@t[2]:@t[1]:@t[0]");
-}
-
-sub fmt_user {
-        local($user) = @_;
-        return( $user.substr($space,0,8 - length($user) ).'  ' );
-}
-
diff --git a/etc/example-direct-cardin b/etc/example-direct-cardin
deleted file mode 100755 (executable)
index 1a40972..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/local/bin/perl
-
-###
-# THIS IS FROM CYBERCASH (is there a newer version?)
-###
-
-$paymentserverhost = 'localhost';
-$paymentserverport = 8000;
-$paymentserversecret = 'two-turntables';
-use CCLib qw(sendmserver);
-
-# first lets fake up some data 
-# use time of day and pid to give me my pretend
-# order number
-# you obviously need to get real data from somewhere...
-
-$oid = "test$$"; #fake order number.
-$amount = 'usd 42.42';
-$ramount = 'usd 24.24';
-$pan = '4111111111111111';
-$name = 'John Q. Doe';
-$addr = '17 Richard Rd.';
-$city = 'Ivyland';
-$state = 'PA';
-$zip = '18974';
-$country = 'USA';
-$exp = '7/97';
-
-
-%result = &sendmserver('mauthcapture', 
-                       'Order-ID', $oid,
-                      'Amount', $amount,
-                      'Card-Number', $pan,
-                      'Card-Name', $name,
-                      'Card-Address', $addr,
-                      'Card-City', $city,
-                      'Card-State', $state,
-                      'Card-Zip', $zip,
-                      'Card-Country', $country,
-                      'Card-Exp', $exp);
-
-#
-# just dump results to stdout.
-# you should process them...
-# to allow results to affect operation of your fulfillment...
-#
-foreach (keys(%result)) {
-   print " $_ ==> $result{$_}\n";
-}
-
-print "\n";
-
-exit;
-
-$trans=$result{'MTransactionNumber'};
-$code=$result{'MRetrievalCode'};
-
-%result = &sendmserver('return',
-                        'Order-ID', $oid,
-                       'Return-Amount',$ramount,
-                       'Amount',$amount,
-                       );
-
-foreach (keys(%result)) {
-   print " $_ ==> $result{$_}\n";
-}
-
diff --git a/etc/megapop.pl b/etc/megapop.pl
new file mode 100755 (executable)
index 0000000..b250bcd
--- /dev/null
@@ -0,0 +1,116 @@
+#!/usr/bin/perl -Tw
+#
+# $Id: megapop.pl,v 1.1 1999-04-19 10:32:44 ivan Exp $
+#
+# this will break when megapop changes the URL or format of their listing page.
+# that's stupid.  perhaps they can provide a machine-readable listing?
+
+use strict;
+use LWP::UserAgent;
+use FS::UID qw(adminsuidsetup);
+use FS::svc_acct_pop;
+
+my $url = "http://www.megapop.com/location.htm";
+
+my $user = shift or die &usage;
+adminsuidsetup($user);
+
+my %state2usps = &state2usps;
+$state2usps{'WASHINGTON STATE'} = 'WA'; #megapop's on crack
+$state2usps{'CANADA'} = 'CANADA'; #freeside's on crack
+
+my $ua = new LWP::UserAgent;
+my $request = new HTTP::Request('GET', $url);
+my $response = $ua->request($request);
+die $response->error_as_HTML unless $response->is_success;
+my $line;
+my $usps = '';
+foreach $line ( split("\n", $response->content) ) {
+  if ( $line =~ /\W(\w[\w\s]*\w)\s+LOCATIONS/i ) {
+    $usps = $state2usps{uc($1)}
+      or warn "warning: unknown state $1\n";
+  } elsif ( $line =~ /(\d{3})\-(\d{3})\-(\d{4})\s+(\w[\w\s]*\w)/ ) {
+    print "$1 $2 $3 $4 $usps\n";
+    my $svc_acct_pop = new FS::svc_acct_pop ( {
+      'city' => $4,
+      'state' => $usps,
+      'ac' => $1,
+      'exch' => $2,
+    } );
+    my $error = $svc_acct_pop->insert;
+    die $error if $error;
+  }
+}
+
+sub usage {
+  die "Usage:\n  $0 user\n";
+}
+
+sub state2usps{ (
+  'ALABAMA' => 'AL',
+  'ALASKA' => 'AK',
+  'AMERICAN SAMOA' => 'AS',
+  'ARIZONA' => 'AZ',
+  'ARKANSAS' => 'AR',
+  'CALIFORNIA' => 'CA',
+  'COLORADO' => 'CO',
+  'CONNECTICUT' => 'CT',
+  'DELAWARE' => 'DE',
+  'DISTRICT OF COLUMBIA' => 'DC',
+  'FEDERATED STATES OF MICRONESIA' => 'FM',
+  'FLORIDA' => 'FL',
+  'GEORGIA' => 'GA',
+  'GUAM' => 'GU',
+  'HAWAII' => 'HI',
+  'IDAHO' => 'ID',
+  'ILLINOIS' => 'IL',
+  'INDIANA' => 'IN',
+  'IOWA' => 'IA',
+  'KANSAS' => 'KS',
+  'KENTUCKY' => 'KY',
+  'LOUISIANA' => 'LA',
+  'MAINE' => 'ME',
+  'MARSHALL ISLANDS' => 'MH',
+  'MARYLAND' => 'MD',
+  'MASSACHUSETTS' => 'MA',
+  'MICHIGAN' => 'MI',
+  'MINNESOTA' => 'MN',
+  'MISSISSIPPI' => 'MS',
+  'MISSOURI' => 'MO',
+  'MONTANA' => 'MT',
+  'NEBRASKA' => 'NE',
+  'NEVADA' => 'NV',
+  'NEW HAMPSHIRE' => 'NH',
+  'NEW JERSEY' => 'NJ',
+  'NEW MEXICO' => 'NM',
+  'NEW YORK' => 'NY',
+  'NORTH CAROLINA' => 'NC',
+  'NORTH DAKOTA' => 'ND',
+  'NORTHERN MARIANA ISLANDS' => 'MP',
+  'OHIO' => 'OH',
+  'OKLAHOMA' => 'OK',
+  'OREGON' => 'OR',
+  'PALAU' => 'PW',
+  'PENNSYLVANIA' => 'PA',
+  'PUERTO RICO' => 'PR',
+  'RHODE ISLAND' => 'RI',
+  'SOUTH CAROLINA' => 'SC',
+  'SOUTH DAKOTA' => 'SD',
+  'TENNESSEE' => 'TN',
+  'TEXAS' => 'TX',
+  'UTAH' => 'UT',
+  'VERMONT' => 'VT',
+  'VIRGIN ISLANDS' => 'VI',
+  'VIRGINIA' => 'VA',
+  'WASHINGTON' => 'WA',
+  'WEST VIRGINIA' => 'WV',
+  'WISCONSIN' => 'WI',
+  'WYOMING' => 'WY',
+  'ARMED FORCES AFRICA' => 'AE',
+  'ARMED FORCES AMERICAS' => 'AA',
+  'ARMED FORCES CANADA' => 'AE',
+  'ARMED FORCES EUROPE' => 'AE',
+  'ARMED FORCES MIDDLE EAST' => 'AE',
+  'ARMED FORCES PACIFIC' => 'AP',
+) }
+
diff --git a/etc/sql-reserved-words.txt b/etc/sql-reserved-words.txt
new file mode 100644 (file)
index 0000000..dc507ce
--- /dev/null
@@ -0,0 +1,103 @@
+From http://epoch.cs.berkeley.edu:8000/sequoia/dba/montage/FAQ/SQL.html
+  by Jean Anderson (jta@postgres.berkeley.edu)
+
+What are the SQL reserved words? 
+
+I grep'd the following list out of the sql docs available via anonymous ftp to speckle.ncsl.nist.gov:/isowg3.
+SQL3 words are not set in stone, but you'd do well to avoid them. 
+
+    From sql1992.txt:
+
+         AFTER, ALIAS, ASYNC, BEFORE, BOOLEAN, BREADTH,
+         COMPLETION, CALL, CYCLE, DATA, DEPTH, DICTIONARY, EACH, ELSEIF,
+         EQUALS, GENERAL, IF, IGNORE, LEAVE, LESS, LIMIT, LOOP, MODIFY,
+         NEW, NONE, OBJECT, OFF, OID, OLD, OPERATION, OPERATORS, OTHERS,
+         PARAMETERS, PENDANT, PREORDER, PRIVATE, PROTECTED, RECURSIVE, REF,
+         REFERENCING, REPLACE, RESIGNAL, RETURN, RETURNS, ROLE, ROUTINE,
+         ROW, SAVEPOINT, SEARCH, SENSITIVE, SEQUENCE, SIGNAL, SIMILAR,
+         SQLEXCEPTION, SQLWARNING, STRUCTURE, TEST, THERE, TRIGGER, TYPE,
+         UNDER, VARIABLE, VIRTUAL, VISIBLE, WAIT, WHILE, WITHOUT
+
+    From sql1992.txt (Annex E):
+
+         ABSOLUTE, ACTION, ADD, ALLOCATE, ALTER, ARE, ASSERTION, AT, BETWEEN,
+         BIT, BIT
+
+What are the SQL reserved words? 
+
+I grep'd the following list out of the sql docs available via anonymous ftp to speckle.ncsl.nist.gov:/isowg3.
+SQL3 words are not set in stone, but you'd do well to avoid them. 
+
+    From sql1992.txt:
+
+         AFTER, ALIAS, ASYNC, BEFORE, BOOLEAN, BREADTH,
+         COMPLETION, CALL, CYCLE, DATA, DEPTH, DICTIONARY, EACH, ELSEIF,
+         EQUALS, GENERAL, IF, IGNORE, LEAVE, LESS, LIMIT, LOOP, MODIFY,
+         NEW, NONE, OBJECT, OFF, OID, OLD, OPERATION, OPERATORS, OTHERS,
+         PARAMETERS, PENDANT, PREORDER, PRIVATE, PROTECTED, RECURSIVE, REF,
+         REFERENCING, REPLACE, RESIGNAL, RETURN, RETURNS, ROLE, ROUTINE,
+         ROW, SAVEPOINT, SEARCH, SENSITIVE, SEQUENCE, SIGNAL, SIMILAR,
+         SQLEXCEPTION, SQLWARNING, STRUCTURE, TEST, THERE, TRIGGER, TYPE,
+         UNDER, VARIABLE, VIRTUAL, VISIBLE, WAIT, WHILE, WITHOUT
+
+    From sql1992.txt (Annex E):
+
+         ABSOLUTE, ACTION, ADD, ALLOCATE, ALTER, ARE, ASSERTION, AT, BETWEEN,
+         BIT, BIT
+
+What are the SQL reserved words? 
+
+I grep'd the following list out of the sql docs available via anonymous ftp to speckle.ncsl.nist.gov:/isowg3.
+SQL3 words are not set in stone, but you'd do well to avoid them. 
+
+    From sql1992.txt:
+
+         AFTER, ALIAS, ASYNC, BEFORE, BOOLEAN, BREADTH,
+         COMPLETION, CALL, CYCLE, DATA, DEPTH, DICTIONARY, EACH, ELSEIF,
+         EQUALS, GENERAL, IF, IGNORE, LEAVE, LESS, LIMIT, LOOP, MODIFY,
+         NEW, NONE, OBJECT, OFF, OID, OLD, OPERATION, OPERATORS, OTHERS,
+         PARAMETERS, PENDANT, PREORDER, PRIVATE, PROTECTED, RECURSIVE, REF,
+         REFERENCING, REPLACE, RESIGNAL, RETURN, RETURNS, ROLE, ROUTINE,
+         ROW, SAVEPOINT, SEARCH, SENSITIVE, SEQUENCE, SIGNAL, SIMILAR,
+         SQLEXCEPTION, SQLWARNING, STRUCTURE, TEST, THERE, TRIGGER, TYPE,
+         UNDER, VARIABLE, VIRTUAL, VISIBLE, WAIT, WHILE, WITHOUT
+
+    From sql1992.txt (Annex E):
+
+         ABSOLUTE, ACTION, ADD, ALLOCATE, ALTER, ARE, ASSERTION, AT, BETWEEN,
+         BIT, BIT_LENGTH, BOTH, CASCADE, CASCADED, CASE, CAST, CATALOG,
+         CHAR_LENGTH, CHARACTER_LENGTH, COALESCE, COLLATE, COLLATION, COLUMN,
+         CONNECT, CONNECTION, CONSTRAINT, CONSTRAINTS, CONVERT, CORRESPONDING,
+         CROSS, CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP, CURRENT_USER,
+         DATE, DAY, DEALLOCATE, DEFERRABLE, DEFERRED, DESCRIBE, DESCRIPTOR,
+         DIAGNOSTICS, DISCONNECT, DOMAIN, DROP, ELSE, END-EXEC, EXCEPT,
+         EXCEPTION, EXECUTE, EXTERNAL, EXTRACT, FALSE, FIRST, FULL, GET,
+         GLOBAL, HOUR, IDENTITY, IMMEDIATE, INITIALLY, INNER, INPUT,
+         INSENSITIVE, INTERSECT, INTERVAL, ISOLATION, JOIN, LAST, LEADING,
+         LEFT, LEVEL, LOCAL, LOWER, MATCH, MINUTE, MONTH, NAMES, NATIONAL,
+         NATURAL, NCHAR, NEXT, NO, NULLIF, OCTET_LENGTH, ONLY, OUTER, OUTPUT,
+         OVERLAPS, PAD, PARTIAL, POSITION, PREPARE, PRESERVE, PRIOR, READ,
+         RELATIVE, RESTRICT, REVOKE, RIGHT, ROWS, SCROLL, SECOND, SESSION,
+         SESSION_USER, SIZE, SPACE, SQLSTATE, SUBSTRING, SYSTEM_USER,
+         TEMPORARY, THEN, TIME, TIMESTAMP, TIMEZONE_HOUR, TIMEZONE_MINUTE,
+         TRAILING, TRANSACTION, TRANSLATE, TRANSLATION, TRIM, TRUE, UNKNOWN,
+         UPPER, USAGE, USING, VALUE, VARCHAR, VARYING, WHEN, WRITE, YEAR, ZONE
+
+    From sql3part2.txt (Annex E)
+
+         ACTION, ACTOR, AFTER, ALIAS, ASYNC, ATTRIBUTES, BEFORE, BOOLEAN,
+         BREADTH, COMPLETION, CURRENT_PATH, CYCLE, DATA, DEPTH, DESTROY,
+         DICTIONARY, EACH, ELEMENT, ELSEIF, EQUALS, FACTOR, GENERAL, HOLD,
+         IGNORE, INSTEAD, LESS, LIMIT, LIST, MODIFY, NEW, NEW_TABLE, NO,
+         NONE, OFF, OID, OLD, OLD_TABLE, OPERATION, OPERATOR, OPERATORS,
+         PARAMETERS, PATH, PENDANT, POSTFIX, PREFIX, PREORDER, PRIVATE,
+         PROTECTED, RECURSIVE, REFERENCING, REPLACE, ROLE, ROUTINE, ROW,
+         SAVEPOINT, SEARCH, SENSITIVE, SEQUENCE, SESSION, SIMILAR, SPACE,
+         SQLEXCEPTION, SQLWARNING, START, STATE, STRUCTURE, SYMBOL, TERM,
+         TEST, THERE, TRIGGER, TYPE, UNDER, VARIABLE, VIRTUAL, VISIBLE,
+         WAIT, WITHOUT
+
+    sql3part4.txt (ANNEX E):
+
+         CALL, DO, ELSEIF, EXCEPTION, IF, LEAVE, LOOP, OTHERS, RESIGNAL,
+         RETURN, RETURNS, SIGNAL, TUPLE, WHILE
index bcf09f1..0b467ae 100755 (executable)
@@ -20,7 +20,7 @@ use vars qw($opt_f $opt_s);
 my($fs_passwdd_socket)="/usr/local/freeside/fs_passwdd_socket";
 my($freeside_uid)=scalar(getpwnam('freeside'));
 
-$ENV{'PATH'} ='/usr/bin:/usr/ucb:/bin';
+$ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
 $ENV{'SHELL'} = '/bin/sh';
 $ENV{'IFS'} = " \t\n";
 $ENV{'CDPATH'} = '';
diff --git a/fs_passwd/fs_passwd.cgi b/fs_passwd/fs_passwd.cgi
new file mode 100755 (executable)
index 0000000..3f676ff
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use Getopt::Std;
+use Socket;
+use IO::Handle;
+use CGI;
+use CGI::Carp qw(fatalsToBrowser);
+
+my $fs_passwdd_socket = "/usr/local/freeside/fs_passwdd_socket";
+my $freeside_uid = scalar(getpwnam('freeside'));
+
+$ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
+$ENV{'SHELL'} = '/bin/sh';
+$ENV{'IFS'} = " \t\n";
+$ENV{'CDPATH'} = '';
+$ENV{'ENV'} = '';
+$ENV{'BASH_ENV'} = '';
+
+die "fs_passwd.cgi isn't running as freeside user\n" if $> != $freeside_uid;
+
+my $cgi = new CGI;
+
+$cgi->param('username') =~ /^([^\n]{0,255}$)/ or die "Illegal username";
+my $me = $1;
+
+$cgi->param('old_password') =~ /^([^\n]{0,255}$)/ or die "Illegal old_password";
+my $old_password = $1;
+
+$cgi->param('new_password') =~ /^([^\n]{0,255}$)/ or die "Illegal new_password";
+my $new_password = $1;
+
+die "New passwords don't match"
+  unless $new_password eq $cgi->param('new_password2');
+
+socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+connect(SOCK, sockaddr_un($fs_passwdd_socket)) or die "connect: $!";
+print SOCK join("\n", $me, $old_password, $new_password, '', ''), "\n";
+SOCK->flush;
+my $error = <SOCK>;
+chomp $error;
+
+if ($error) {
+  die $error;
+} else {
+  print $cgi->header(), <<END;
+<html>
+  <head>
+    <title>Password changed</title>
+  </head>
+  <body bgcolor="#e8e8e8">
+    <h3>Password changed</h3>
+<br>Your password has been changed.
+  </body>
+</html>
+END
+}
diff --git a/fs_passwd/fs_passwd.html b/fs_passwd/fs_passwd.html
new file mode 100644 (file)
index 0000000..fadc4df
--- /dev/null
@@ -0,0 +1,25 @@
+<html>
+  <head>
+    <title>Change password</title>
+  </head>
+  <body bgcolor="#e8e8e8">
+    <h3>Change password</h3>
+    <form action="/cgi-bin/fs_passwd.cgi" method="post">
+    <table bgcolor="#cccccc" border=0 cellspacing=2>
+      <tr><th align="right">Username</th>
+        <td><input type="text" name="username" size="18"></td>
+      </tr>
+      <tr><th align="right">Current password</th>
+        <td><input type="password" name="old_password" size="18"></td>
+      </tr>
+      <tr><th align="right">New password</th>
+        <td><input type="password" name="new_password" size="18"></td>
+      </tr>
+      <tr><th align="right">Re-enter new password</th>
+        <td><input type="password" name="new_password2" size="18"></td>
+      </tr>
+    </table>
+    <br><input type="submit" value="Change password">
+  </body>
+</html>
+
index 99e7c43..a29b2c7 100755 (executable)
 # crypt-aware, s/password/_password/; ivan@sisd.com 98-aug-23
 
 use strict;
+use vars qw($pid);
+use subs qw(killssh);
 use IO::Handle;
-use FS::SSH qw(sshopen2);
+use Net::SSH qw(sshopen2);
 use FS::UID qw(adminsuidsetup);
 use FS::Record qw(qsearchs);
 use FS::svc_acct;
 
-$SIG{CHLD} = sub { wait() };
+my $user = shift or die &usage;
+adminsuidsetup $user; 
 
-&adminsuidsetup; 
+my($shellmachine)=shift or die &usage;
 
-my($fs_passwdd)="/usr/local/sbin/fs_passwdd";
+#causing trouble for some folks
+#$SIG{CHLD} = sub { wait() };
+
+$SIG{HUP} = \&killssh;
+$SIG{INT} = \&killssh;
+$SIG{QUIT} = \&killssh;
+$SIG{TERM} = \&killssh;
+$SIG{PIPE} = \&killssh;
+
+sub killssh { kill 'TERM', $pid if $pid; exit; };
 
-my($shellmachine)=shift;
-die "Usage: fs_passwd_server shellmachine\n" unless $shellmachine;
+my($fs_passwdd)="/usr/local/sbin/fs_passwdd";
 
 while (1) {
   my($reader,$writer)=(new IO::Handle, new IO::Handle);
   $writer->autoflush(1);
-  sshopen2($shellmachine,$reader,$writer,$fs_passwdd);
+  $pid = sshopen2($shellmachine,$reader,$writer,$fs_passwdd);
   while (1) {
     my($username,$old_password,$new_password,$new_gecos,$new_shell);
     defined($username=<$reader>) or last;
@@ -57,7 +68,7 @@ while (1) {
     unless ( $svc_acct ) { print $writer "Incorrect password.\n"; next; }
 
     my(%hash)=$svc_acct->hash;
-    my($new_svc_acct) = create FS::svc_acct ( \%hash );
+    my($new_svc_acct) = new FS::svc_acct ( \%hash );
     $new_svc_acct->setfield('_password',$new_password) 
       if $new_password && $new_password ne $old_password;
     $new_svc_acct->setfield('finger',$new_gecos) if $new_gecos;
@@ -71,3 +82,7 @@ while (1) {
   warn "Connection to $shellmachine lost!  Reconnecting...\n";
 }
 
+sub usage {
+  die "Usage:\n\n  fs_passwd_server user shellmachine\n";
+}
+
index 582e13c..cce98e7 100755 (executable)
@@ -9,9 +9,10 @@
 use strict;
 use Socket;
 
-my($fs_passwdd_socket)="/usr/local/freeside/fs_passwdd_socket";
+my $fs_passwdd_socket = "/usr/local/freeside/fs_passwdd_socket";
+my $pid_file = "$fs_passwdd_socket.pid";
 
-$ENV{'PATH'} ='/usr/bin:/usr/ucb:/bin';
+$ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
 $ENV{'SHELL'} = '/bin/sh';
 $ENV{'IFS'} = " \t\n";
 $ENV{'CDPATH'} = '';
@@ -28,6 +29,18 @@ unlink($fs_passwdd_socket);
 bind(Server, $uaddr) or die "bind: $!";
 listen(Server,SOMAXCONN) or die "listen: $!";
 
+if ( -e $pid_file ) {
+  open(PIDFILE,"<$pid_file");
+  #chomp( my $old_pid = <PIDFILE> );
+  my $old_pid = <PIDFILE>;
+  close PIDFILE;
+  $old_pid =~ /^(\d+)$/;
+  kill 'TERM', $1;
+}
+open(PIDFILE,">$pid_file");
+print PIDFILE "$$\n";
+close PIDFILE;
+
 my($paddr);
 for ( ; $paddr = accept(Client,Server); close Client) {
   my($me,$old_password,$new_password,$new_gecos,$new_shell);
diff --git a/fs_selfadmin/FS-MailAdminServer/MailAdminClient.pm b/fs_selfadmin/FS-MailAdminServer/MailAdminClient.pm
new file mode 100755 (executable)
index 0000000..46cde4c
--- /dev/null
@@ -0,0 +1,541 @@
+package FS::MailAdminClient;
+
+use strict;
+use vars qw($VERSION @ISA @EXPORT_OK $fs_mailadmind_socket);
+use Exporter;
+use Socket;
+use FileHandle;
+use IO::Handle;
+
+$VERSION = '0.01';
+
+@ISA = qw( Exporter );
+@EXPORT_OK = qw( signup_info authenticate list_packages list_mailboxes delete_mailbox password_mailbox add_mailbox list_forwards list_pkg_forwards delete_forward add_forward new_customer );
+
+$fs_mailadmind_socket = "/usr/local/freeside/fs_mailadmind_socket";
+
+$ENV{'PATH'} ='/usr/bin:/usr/ucb:/bin';
+$ENV{'SHELL'} = '/bin/sh';
+$ENV{'IFS'} = " \t\n";
+$ENV{'CDPATH'} = '';
+$ENV{'ENV'} = '';
+$ENV{'BASH_ENV'} = '';
+
+my $freeside_uid = scalar(getpwnam('freeside'));
+die "not running as the freeside user\n" if $> != $freeside_uid;
+
+=head1 NAME
+
+FS::MailAdminClient - Freeside mail administration client API
+
+=head1 SYNOPSIS
+
+  use FS::MailAdminClient qw( signup_info list_mailboxes  new_customer );
+
+  ( $locales, $packages, $pops ) = signup_info;
+
+  ( $accounts ) = list_mailboxes;
+
+  $error = new_customer ( {
+    'first'          => $first,
+    'last'           => $last,
+    'ss'             => $ss,
+    'comapny'        => $company,
+    'address1'       => $address1,
+    'address2'       => $address2,
+    'city'           => $city,
+    'county'         => $county,
+    'state'          => $state,
+    'zip'            => $zip,
+    'country'        => $country,
+    'daytime'        => $daytime,
+    'night'          => $night,
+    'fax'            => $fax,
+    'payby'          => $payby,
+    'payinfo'        => $payinfo,
+    'paydate'        => $paydate,
+    'payname'        => $payname,
+    'invoicing_list' => $invoicing_list,
+    'pkgpart'        => $pkgpart,
+    'username'       => $username,
+    '_password'       => $password,
+    'popnum'         => $popnum,
+  } );
+
+=head1 DESCRIPTION
+
+This module provides an API for a remote mail administration server.
+
+It needs to be run as the freeside user.  Because of this, the program which
+calls these subroutines should be written very carefully.
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item signup_info
+
+Returns three array references of hash references.
+
+The first set of hash references is of allowable locales.  Each hash reference
+has the following keys:
+  taxnum
+  state
+  county
+  country
+
+The second set of hash references is of allowable packages.  Each hash
+reference has the following keys:
+  pkgpart
+  pkg
+
+The third set of hash references is of allowable POPs (Points Of Presence).
+Each hash reference has the following keys:
+  popnum
+  city
+  state
+  ac
+  exch
+
+=cut
+
+sub signup_info {
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "signup_info\n";
+  SOCK->flush;
+
+  chop ( my $n_cust_main_county = <SOCK> );
+  my @cust_main_county = map {
+    chop ( my $taxnum  = <SOCK> ); 
+    chop ( my $state   = <SOCK> ); 
+    chop ( my $county  = <SOCK> ); 
+    chop ( my $country = <SOCK> );
+    {
+      'taxnum'  => $taxnum,
+      'state'   => $state,
+      'county'  => $county,
+      'country' => $country,
+    };
+  } 1 .. $n_cust_main_county;
+
+  chop ( my $n_part_pkg = <SOCK> );
+  my @part_pkg = map {
+    chop ( my $pkgpart = <SOCK> ); 
+    chop ( my $pkg     = <SOCK> ); 
+    {
+      'pkgpart' => $pkgpart,
+      'pkg'     => $pkg,
+    };
+  } 1 .. $n_part_pkg;
+
+  chop ( my $n_svc_acct_pop = <SOCK> );
+  my @svc_acct_pop = map {
+    chop ( my $popnum = <SOCK> ); 
+    chop ( my $city   = <SOCK> ); 
+    chop ( my $state  = <SOCK> ); 
+    chop ( my $ac     = <SOCK> );
+    chop ( my $exch   = <SOCK> );
+    chop ( my $loc    = <SOCK> );
+    {
+      'popnum' => $popnum,
+      'city'   => $city,
+      'state'  => $state,
+      'ac'     => $ac,
+      'exch'   => $exch,
+      'loc'    => $loc,
+    };
+  } 1 .. $n_svc_acct_pop;
+
+  close SOCK;
+
+  \@cust_main_county, \@part_pkg, \@svc_acct_pop;
+}
+
+=item authenticate
+
+Authentictes against a service on the remote Freeside system.  Requires a hash
+reference as a parameter with the following keys:
+    authuser
+    _password
+
+Returns a scalar error message of the form "authuser OK|FAILED" or an error
+message.
+
+=cut
+
+sub authenticate {
+  my $hashref = shift;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "authenticate", "\n";
+  SOCK->flush;
+
+  print SOCK join("\n", map { $hashref->{$_} } qw(
+    authuser _password
+  ) ), "\n";
+  SOCK->flush;
+
+  chop( my $error = <SOCK> );
+  close SOCK;
+
+  $error;
+}
+
+=item list_packages
+
+Returns one array reference of hash references.
+
+The set of hash references is of existing packages.  Each hash reference
+has the following keys:
+  pkgnum
+  domain
+  account
+
+=cut
+
+sub list_packages {
+  my $user = shift;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "list_packages\n", $user, "\n";
+  SOCK->flush;
+
+  chop ( my $n_packages = <SOCK> );
+  my @packages = map {
+    chop ( my $pkgnum  = <SOCK> ); 
+    chop ( my $domain  = <SOCK> ); 
+    chop ( my $account = <SOCK> ); 
+    {
+      'pkgnum'  => $pkgnum,
+      'domain'  => $domain,
+      'account' => $account,
+    };
+  } 1 .. $n_packages;
+
+  close SOCK;
+
+  \@packages;
+}
+
+=item list_mailboxes
+
+Returns one array references of hash references.
+
+The set of hash references is of existing accounts.  Each hash reference
+has the following keys:
+  svcnum
+  username
+  _password
+
+=cut
+
+sub list_mailboxes {
+  my ($user, $package) = @_;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "list_mailboxes\n", $user, "\n", $package, "\n";
+  SOCK->flush;
+
+  chop ( my $n_svc_acct = <SOCK> );
+  my @svc_acct = map {
+    chop ( my $svcnum  = <SOCK> ); 
+    chop ( my $username  = <SOCK> ); 
+    chop ( my $_password   = <SOCK> ); 
+    {
+      'svcnum'  => $svcnum,
+      'username'  => $username,
+      '_password'   => $_password,
+    };
+  } 1 .. $n_svc_acct;
+
+  close SOCK;
+
+  \@svc_acct;
+}
+
+=item delete_mailbox
+
+Deletes a mailbox service from the remote Freeside system.  Requires a hash
+reference as a paramater with the following keys:
+    authuser
+    account
+
+Returns a scalar error message, or the empty string for success.
+
+=cut
+
+sub delete_mailbox {
+  my $hashref = shift;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "delete_mailbox", "\n";
+  SOCK->flush;
+
+  print SOCK join("\n", map { $hashref->{$_} } qw(
+    authuser account
+  ) ), "\n";
+  SOCK->flush;
+
+  chop( my $error = <SOCK> );
+  close SOCK;
+
+  $error;
+}
+
+=item password_mailbox
+
+Changes the password for a mailbox service on the remote Freeside system.
+  Requires a hash reference as a paramater with the following keys:
+    authuser
+    account
+    _password
+
+Returns a scalar error message, or the empty string for success.
+
+=cut
+
+sub password_mailbox {
+  my $hashref = shift;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "password_mailbox", "\n";
+  SOCK->flush;
+
+  print SOCK join("\n", map { $hashref->{$_} } qw(
+    authuser account _password
+  ) ), "\n";
+  SOCK->flush;
+
+  chop( my $error = <SOCK> );
+  close SOCK;
+
+  $error;
+}
+
+=item add_mailbox
+
+Creates a mailbox service on the remote Freeside system.  Requires a hash
+reference as a parameter with the following keys:
+    authuser
+    package
+    account
+    _password
+
+Returns a scalar error message, or the empty string for success.
+
+=cut
+
+sub add_mailbox {
+  my $hashref = shift;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "add_mailbox", "\n";
+  SOCK->flush;
+
+  print SOCK join("\n", map { $hashref->{$_} } qw(
+    authuser package account _password
+  ) ), "\n";
+  SOCK->flush;
+
+  chop( my $error = <SOCK> );
+  close SOCK;
+
+  $error;
+}
+
+=item list_forwards
+
+Returns one array references of hash references.
+
+The set of hash references is of existing forwards.  Each hash reference
+has the following keys:
+  svcnum
+  dest
+
+=cut
+
+sub list_forwards {
+  my ($user, $service) = @_;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "list_forwards\n", $user, "\n", $service, "\n";
+  SOCK->flush;
+
+  chop ( my $n_svc_forward = <SOCK> );
+  my @svc_forward = map {
+    chop ( my $svcnum  = <SOCK> ); 
+    chop ( my $dest  = <SOCK> ); 
+    {
+      'svcnum'  => $svcnum,
+      'dest'  => $dest,
+    };
+  } 1 .. $n_svc_forward;
+
+  close SOCK;
+
+  \@svc_forward;
+}
+
+=item list_pkg_forwards
+
+Returns one array references of hash references.
+
+The set of hash references is of existing forwards.  Each hash reference
+has the following keys:
+  svcnum
+  srcsvc
+  dest
+
+=cut
+
+sub list_pkg_forwards {
+  my ($user, $package) = @_;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "list_pkg_forwards\n", $user, "\n", $package, "\n";
+  SOCK->flush;
+
+  chop ( my $n_svc_forward = <SOCK> );
+  my @svc_forward = map {
+    chop ( my $svcnum  = <SOCK> ); 
+    chop ( my $srcsvc  = <SOCK> ); 
+    chop ( my $dest  = <SOCK> ); 
+    {
+      'svcnum'  => $svcnum,
+      'srcsvc'  => $srcsvc,
+      'dest'  => $dest,
+    };
+  } 1 .. $n_svc_forward;
+
+  close SOCK;
+
+  \@svc_forward;
+}
+
+=item delete_forward
+
+Deletes a forward service from the remote Freeside system.  Requires a hash
+reference as a paramater with the following keys:
+    authuser
+    svcnum
+
+Returns a scalar error message, or the empty string for success.
+
+=cut
+
+sub delete_forward {
+  my $hashref = shift;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "delete_forward", "\n";
+  SOCK->flush;
+
+  print SOCK join("\n", map { $hashref->{$_} } qw(
+    authuser svcnum
+  ) ), "\n";
+  SOCK->flush;
+
+  chop( my $error = <SOCK> );
+  close SOCK;
+
+  $error;
+}
+
+=item add_forward
+
+Creates a forward service on the remote Freeside system.  Requires a hash
+reference as a parameter with the following keys:
+    authuser
+    package
+    source
+    dest
+
+Returns a scalar error message, or the empty string for success.
+
+=cut
+
+sub add_forward {
+  my $hashref = shift;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "add_forward", "\n";
+  SOCK->flush;
+
+  print SOCK join("\n", map { $hashref->{$_} } qw(
+    authuser package source dest
+  ) ), "\n";
+  SOCK->flush;
+
+  chop( my $error = <SOCK> );
+  close SOCK;
+
+  $error;
+}
+
+=item new_customer HASHREF
+
+Adds a customer to the remote Freeside system.  Requires a hash reference as
+a paramater with the following keys:
+  first
+  last
+  ss
+  comapny
+  address1
+  address2
+  city
+  county
+  state
+  zip
+  country
+  daytime
+  night
+  fax
+  payby
+  payinfo
+  paydate
+  payname
+  invoicing_list
+  pkgpart
+  username
+  _password
+  popnum
+
+Returns a scalar error message, or the empty string for success.
+
+=cut
+
+sub new_customer {
+  my $hashref = shift;
+
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_mailadmind_socket)) or die "connect: $!";
+  print SOCK "new_customer\n";
+
+  print SOCK join("\n", map { $hashref->{$_} } qw(
+    first last ss company address1 address2 city county state zip country
+    daytime night fax payby payinfo paydate payname invoicing_list
+    pkgpart username _password popnum
+  ) ), "\n";
+  SOCK->flush;
+
+  chop( my $error = <SOCK> );
+  $error;
+}
+
+=back
+
+=head1 VERSION
+
+$Id: MailAdminClient.pm,v 1.1 2001-10-18 15:04:54 jeff Exp $
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<fs_signupd>, L<FS::SignupServer>, L<FS::cust_main>
+
+=cut
+
+1;
+
diff --git a/fs_selfadmin/FS-MailAdminServer/cgi/mailadmin.cgi b/fs_selfadmin/FS-MailAdminServer/cgi/mailadmin.cgi
new file mode 100755 (executable)
index 0000000..c26c3dc
--- /dev/null
@@ -0,0 +1,698 @@
+#!/usr/bin/perl
+########################################################################
+#                                                                      #
+#    mailadmin.cgi                NCI2000                              #
+#                                 Jeff Finucane <jeff@nci2000.net>     #
+#                                 26 April 2001                        #
+#                                                                      #
+########################################################################
+
+use DBI;
+use strict;
+use CGI;
+use FS::MailAdminClient qw(authenticate list_packages list_mailboxes delete_mailbox password_mailbox add_mailbox list_forwards list_pkg_forwards delete_forward add_forward);
+
+my $sessionfile = '/usr/local/apache/htdocs/mailadmin/adminsess';   # session file
+my $tmpdir = '/usr/local/apache/htdocs/mailadmin/tmp';         # Location to store temp files
+my $cookiedomain = ".your.dom";      # domain if THIS server, should prepend with a '.'
+my $cookieexpire = '+12h';              # expire the cookie session after this much idle time
+my $sessexpire = 43200;                 # expire session after this long of no use (in seconds)
+
+my $body = "<body bgcolor=dddddd>";
+
+#### Should not have to change anything under this line ####
+my $printmainpage = 1;
+my $i = 0;
+my $printheader = 1;
+my $query = new CGI;
+my $cgi = $query->url();
+my $now = getdatetime();
+my $current_package = 0;
+my $current_account = 0;
+my $current_domname = "";
+
+# if they are trying to login we wont check the session yet
+if ($query->param('login') eq '' && $query->param('action') ne 'login') {
+  checksession();
+  printheader();
+}
+
+if ($query->param('login') ne '') {
+
+   my $username = $query->param('username');
+   my $password = $query->param('password');
+
+   if (!checkuserpass($username, $password)) {
+      printheader();
+      error('not_admin');
+   }
+
+   my @alpha = ('A'..'Z', 'a'..'z', 0..9);
+   my $sessid = '';
+   for (my $i = 0; $i < 10; $i++) {
+       $sessid .= @alpha[rand(@alpha)];
+   }
+
+   my $cookie1 = $query->cookie(-name=>'username',
+                               -value=>$username,
+                               -expires=>$cookieexpire,
+                               -domain=>$cookiedomain);
+
+   my $cookie2 = $query->cookie(-name=>'ma_sessionid',
+                               -value=>$sessid,
+                               -expires=>$cookieexpire,
+                               -domain=>$cookiedomain);
+
+   my $now = time();
+   open(NEWSESS, ">>$sessionfile") || error('open');
+   print NEWSESS "$username $sessid $now 0 0\n";
+   close(NEWSESS);
+
+   print $query->header(-COOKIE=>[$cookie1, $cookie2]);
+   $printmainpage = 1;
+
+} elsif ($query->param('action') eq 'blankframe') {
+   
+  print "<html>$body</body></html>\n";
+   $printmainpage = 0;
+
+} elsif ($query->param('action') eq 'list_packages') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  my $list = list_packages($username);
+  print "<html>$body\n";
+  print "<center><table border=0>\n";
+  print "<tr><td></td><td><p>Package Number</td><td><p>Description</td></tr>\n";
+  foreach my $package ( @{$list} ) {
+    print "<tr>";
+    print "<td></td><td><p>$package->{'pkgnum'}</td><td><p>$package->{'domain'}</td>\n";
+    print "<td></td><td><a href=\"$cgi\?action=select&package=$package->{'pkgnum'}&account=$package->{'account'}&domname=$package->{'domain'}\" target=\"rightmainframe\">select</td>\n";
+    print "</tr>";
+  }
+  print "</table>\n";
+  print "</body></html>\n";
+  $printmainpage=0;
+
+} elsif ($query->param('action') eq 'list_mailboxes') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username)  unless $current_package;
+  my $list = list_mailboxes($username, $current_package);
+  my $forwardlist = list_pkg_forwards($username, $current_package);
+  print "<html>$body\n";
+  print "<center><table border=0>\n";
+  print "<tr><td></td><td><p>Username</td><td><p>Password</td></tr>\n";
+  foreach my $account ( @{$list} ) {
+    print "<tr>";
+    print "<td></td><td><p>$account->{'username'}</td><td><p>$account->{'_password'}</td>\n";
+    print "<td></td><td><a href=\"$cgi\?action=change&account=$account->{'svcnum'}&mailbox=$account->{'username'}\" target=\"rightmainframe\">change</td>\n";
+    print "</tr>";
+
+#    my $forwardlist = list_forwards($username, $account->{'svcnum'});
+#    foreach my $forward ( @{$forwardlist} ) {
+#      my $label = qq!=> ! . $forward->{'dest'};
+#      print "<tr><td></td><td></td><td><p>$label</td></tr>\n";
+#    }
+    foreach my $forward ( @{$forwardlist} ) {
+      if ($forward->{'srcsvc'} == $account->{'svcnum'}) {
+        my $label = qq!=> ! . $forward->{'dest'};
+        print "<tr><td></td><td></td><td><p>$label</td></tr>\n";
+      }
+    }
+
+  }
+  print "</table>\n";
+  print "</body></html>\n";
+  $printmainpage=0;
+
+} elsif ($query->param('action') eq 'select') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  $current_package = $query->param('package');
+  $current_account = $query->param('account');
+  $current_domname = $query->param('domname');
+  set_package();
+  print "<html>$body\n";
+  print "<form name=form1 action=\"$cgi\" method=post target=\"rightmainframe\">\n";
+  print "<center>\n";
+  print "<p>Selected package $current_package\n";
+  print "</center>\n";
+  print "</form>\n";
+  print "</body></html>\n";
+  $printmainpage=0;
+
+} elsif ($query->param('action') eq 'change') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username) unless $current_package;
+  my $account  = $query->param('account');
+  my $mailbox  = $query->param('mailbox');
+  my $list = list_forwards($username, $account);
+  print "<html>$body\n";
+  print "<form name=form1 action=\"$cgi\" method=post target=\"rightmainframe\">\n";
+  print "<center><table border=0>\n";
+  print "<tr><td></td><td><p>Username</td><td><p>$mailbox</td></tr>\n";
+  print "<input type=hidden name=\"account\" value=\"$account\">\n";
+  print "<input type=hidden name=\"mailbox\" value=\"$mailbox\">\n";
+  foreach my $forward ( @{$list} ) {
+    my $label = qq!=> ! . $forward->{'dest'};
+#    print "<tr><td></td><td></td><td><p>$label</td></tr>\n";
+    print "<tr><td></td><td></td><td><p>$label</td><td><a href=\"$cgi\?action=deleteforward&service=$forward->{'svcnum'}&mailbox=$mailbox&dest=$forward->{'dest'}\" target=\"rightmainframe\">remove</td></tr>\n";
+  }
+  print "<tr><td></td><td><p>Password</td><td><input type=text name=\"_password\" value=\"\"></td></tr>\n";
+  print "</table>\n";
+  print "<input type=submit name=\"deleteaccount\" value=\"Delete This User\">\n";
+  print "<input type=submit name=\"changepassword\" value=\"Change The Password\">\n";
+  print "<input type=submit name=\"addforward\" value=\"Add Forwarding\">\n";
+  print "</center>\n";
+  print "</form>\n";
+  print "<br>\n";
+  print "<p> You may delete this user and all mailforwarding by pressing <B>Delete This User</B>.\n";
+  print "<p> To set or change the password for this user, type the new password in the box next to <B>Password</B> and press <B>Change The Password</B>.\n";
+  print "<p> If you would like to have mail destined for this user forwarded to another email address then press the <B>Add Forwarding</B> button.\n";
+  print "</body></html>\n";
+  $printmainpage=0;
+
+} elsif ($query->param('deleteaccount') ne '') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username) unless $current_package;
+  my $account  = $query->param('account');
+  my $mailbox  = $query->param('mailbox');
+  print "<html>$body\n";
+  print "<form name=form1 action=\"$cgi\" method=post target=\"rightmainframe\">\n";
+  print "<p>Are you certain you want to delete user $mailbox?\n";
+  print "<p><input type=hidden name=\"account\" value=\"$account\">\n";
+  print "<input type=submit name=\"deleteaccounty\" value=\"Confirm\">\n";
+  print "</body></html>\n";
+  $printmainpage=0;
+
+} elsif ($query->param('deleteaccounty') ne '') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username) unless $current_package;
+  my $account  = $query->param('account');
+  
+  if  ( my $error = delete_mailbox ( {
+      'authuser'         => $username,
+      'account'          => $account,
+    } ) ) {
+    print "<html>$body\n";
+    print "<p>$error\n";
+    print "</body></html>\n";
+      
+  } else {
+    print "<html>$body\n";
+    print "<p>Deleted\n";
+    print "</body></html>\n";
+  }
+
+  $printmainpage=0;
+
+} elsif ($query->param('changepassword') ne '') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username) unless $current_package;
+  my $account  = $query->param('account');
+  my $_password  = $query->param('_password');
+  
+  if  ( my $error = password_mailbox ( {
+      'authuser'         => $username,
+      'account'          => $account,
+      '_password'        => $_password,
+    } ) ) {
+    print "<html>$body\n";
+    print "<p>$error\n";
+    print "</body></html>\n";
+      
+  } else {
+    print "<html>$body\n";
+    print "<p>Changed\n";
+    print "</body></html>\n";
+  }
+
+  $printmainpage=0;
+
+} elsif ($query->param('action') eq 'newmailbox') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username) unless $current_package;
+  print "<html>$body\n";
+  print "<form name=form1 action=\"$cgi\" method=post target=\"rightmainframe\">\n";
+  print "<center><table border=0>\n";
+  print "<tr><td></td><td><p>Username </td><td><input type=text name=\"account\" value=\"\"></td><td>@ " . $current_domname . "</td></tr>\n";
+  print "<tr><td></td><td><p>Password</td><td><input type=text name=\"_password\" value=\"\"></td></tr>\n";
+  print "</table>\n";
+  print "<input type=submit name=\"addmailbox\" value=\"Add This User\">\n";
+  print "</center>\n";
+  print "</form>\n";
+  print "<br>\n";
+  print "<p>Use this screen to add a new mailbox user.  If the domain name of the email address (the part after the <B>@</B> sign) is not what you expect then you may need to use <B>List Packages</B> to select the package with the correct domain.\n";
+  print "<p>Enter the first portion of the email address in the box adjacent to <B>Username</B> and enter the password for that user in the space next to <B>Password</B>.  Then press the button labeled <B>Add The User</B>.\n";
+  print "<p>If you do not want to add a new user at this time then select a choice from the menu at the left, such as <B>List Mailboxes</B>.\n";
+  print "</body></html>\n";
+  $printmainpage=0;
+
+} elsif ($query->param('addmailbox') ne '') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username) unless $current_package;
+  my $account  = $query->param('account');
+  my $_password  = $query->param('_password');
+  
+  if  ( my $error = add_mailbox ( {
+      'authuser'         => $username,
+      'package'          => $current_package,
+      'account'          => $account,
+      '_password'        => $_password,
+    } ) ) {
+    print "<html>$body\n";
+    print "<p>$error\n";
+    print "</body></html>\n";
+      
+  } else {
+    print "<html>$body\n";
+    print "<p>Created\n";
+    print "</body></html>\n";
+  }
+
+  $printmainpage=0;
+
+} elsif ($query->param('action') eq 'deleteforward') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username) unless $current_package;
+  my $svcnum   = $query->param('service');
+  my $mailbox  = $query->param('mailbox');
+  my $dest  = $query->param('dest');
+  print "<html>$body\n";
+  print "<form name=form1 action=\"$cgi\" method=post target=\"rightmainframe\">\n";
+  print "<p>Are you certain you want to remove the forwarding from $mailbox to $dest?\n";
+  print "<p><input type=hidden name=\"service\" value=\"$svcnum\">\n";
+  print "<input type=submit name=\"deleteforwardy\" value=\"Confirm\">\n";
+  print "</body></html>\n";
+  $printmainpage=0;
+
+} elsif ($query->param('deleteforwardy') ne '') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username) unless $current_package;
+  my $service  = $query->param('service');
+  
+  if  ( my $error = delete_forward ( {
+      'authuser'        => $username,
+      'svcnum'          => $service,
+    } ) ) {
+    print "<html>$body\n";
+    print "<p>$error\n";
+    print "</body></html>\n";
+      
+  } else {
+    print "<html>$body\n";
+    print "<p>Forwarding Removed\n";
+    print "</body></html>\n";
+  }
+
+  $printmainpage=0;
+
+} elsif ($query->param('addforward') ne '') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username) unless $current_package;
+  my $account  = $query->param('account');
+  my $mailbox  = $query->param('mailbox');
+  
+  print "<html>$body\n";
+  print "<form name=form1 action=\"$cgi\" method=post target=\"rightmainframe\">\n";
+  print "<center><table border=0>\n";
+  print "<input type=hidden name=\"account\" value=\"$account\">\n";
+  print "<input type=hidden name=\"mailbox\" value=\"$mailbox\">\n";
+  print "<tr><td>Forward mail from </td><td><p>$mailbox:</td><td> to </td></tr>\n";
+  print "<tr><td></td><td><p>Destination:</td><td><input type=text name=\"dest\" value=\"\"></td></tr>\n";
+  print "</table>\n";
+  print "<input type=submit name=\"addforwarddst\" value=\"Add the Forwarding\">\n";
+  print "</center>\n";
+  print "</form>\n";
+  print "<br>\n";
+  print "<p> If you would like mail originally destined for the above address to be forwarded to a different email address then type that email address in the box next to <B>Destination:</B> and press the <B>Add the Forwarding</B> button.\n";
+  print "<p> If you do not want to add mail forwarding then select a choice from the menu at the left, such as <B>List Accounts</B>.\n";
+
+  $printmainpage=0;
+
+} elsif ($query->param('addforwarddst') ne '') {
+
+  my $username = $query->cookie(-name=>'username');  # session checked
+  select_package($username) unless $current_package;
+  my $account  = $query->param('account');
+  my $dest  = $query->param('dest');
+  
+  if  ( my $error = add_forward ( {
+      'authuser'         => $username,
+      'package'          => $current_package,
+      'source'           => $account,
+      'dest'             => $dest,
+    } ) ) {
+    print "<html>$body\n";
+    print "<p>$error\n";
+    print "</body></html>\n";
+      
+  } else {
+    print "<html>$body\n";
+    print "<p>Forwarding Created\n";
+    print "</body></html>\n";
+  }
+
+  $printmainpage=0;
+
+} elsif ($query->param('action') eq 'navframe') {
+
+  print "<html><body bgcolor=bbbbbb>\n";
+  print "<center><h2>NCI2000 MAIL ADMIN Web Interface</h2></center>\n";
+
+  print "<br><center>Choose Action:</center><br>\n";
+  print "<center><table border=0>\n";
+  print "<ul>\n";
+  print "<tr><td><li><a href=\"$cgi\?action=logout\" target=\"_top\">Log Off</a></td><tr>\n";
+  print "<tr><td><li><a href=\"$cgi\?action=list_packages\" target=\"rightmainframe\">List Packages</a></td><tr>\n";
+  print "<tr><td><li><a href=\"$cgi\?action=list_mailboxes\" target=\"rightmainframe\">List Accounts</a></td><tr>\n";
+  print "<tr><td><li><a href=\"$cgi\?action=newmailbox\" target=\"rightmainframe\">Add Account</a></td><tr>\n";
+  print "</ul>\n";
+  print "</table></center>\n";
+
+  print "<br><br><br>\n";
+  print "</body></html>\n";
+
+  $printmainpage = 0;
+
+} elsif ($query->param('action') eq 'rightmainframe') {
+
+  print "<html>$body\n";
+  print "<br><br><br>\n";
+  print "<font size=4><----- Please choose function on the left menu</font>\n";
+  print "<br><br>\n";
+  print "<p> Choose <B>Log Off</B> when you are finished.  This helps prevent unauthorized access to your accounts.\n";
+  print "<p> Use <B>List Packages</B> when you administer multiple packages.  When you have multiple domains at NCI2000 you are likely to have multiple packages.  Use of <B>List Packages</B> is not necessary if administer only one package.\n";
+  print "<p> Use <B>List Accounts</B> to view your current arrangement of mailboxes.  From this list you my choose to make changes to existing mailboxes or delete mailboxes.  If you would like to modify the forwarding associated with a mailbox then choose it from this list.\n";
+  print "<p> Use <B>Add Account</B> when you would like an additional mailbox.  After you have added the mailbox you may choose to make additional changes from the list provided by <B>List Accounts<B>.\n";
+  print "</body></html>\n";
+
+  $printmainpage = 0;
+
+}
+
+
+if ($query->param('action') eq 'login') {
+
+    printheader();
+    printlogin();
+
+} elsif ($query->param('action') eq 'logout') {
+
+    destroysession();
+    printheader();
+    printlogin();
+
+} elsif ($printmainpage) {
+
+
+  print "<html><head><title>NCI2000 MAIL ADMIN Web Interface</title></head>\n";
+  print "<FRAMESET cols=\"160,*\" BORDER=\"3\">\n";
+  print "<FRAME NAME=\"navframe\" src=\"$cgi?action=navframe\">\n";
+  print "<FRAME NAME=\"rightmainframe\" src=\"$cgi?action=rightmainframe\">\n";
+  print "</FRAMESET>\n";
+  print "</html>\n";
+
+
+}
+
+sub getdatetime {
+  my $today = localtime(time());
+  my ($day,$mon,$dayofmon,$time,$year) = split(/\s+/,$today);
+  my @datemonths = ("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");
+
+  my $numidx = "01";
+  my ($nummon);
+  foreach my $mons (@datemonths) {
+    if ($mon eq $mons) {
+     $nummon = $numidx;
+    }
+    $numidx++;
+  }
+
+  return "$year-$nummon-$dayofmon $time";
+
+}
+
+sub error {
+
+  my $error = shift;
+  my $arg1 = shift;
+
+   printheader();
+
+   if ($error eq 'not_admin') {
+     print "<html><head><title>Error!</title></head>\n";
+     print "$body\n";
+     print "<center><h1><font face=arial>Error!</font></h1></center>\n";
+     print "<font face=arial>Unauthorized attempt to access mail administration.</font>\n";
+     print "<br><font face=arial>Please login again if you think this is an error.</font>\n";
+     print "<form><input type=button value=\"<<Back\" OnClick=\"history.back()\"></form>\n";
+     print "</body></html>\n";
+   } elsif ($error eq 'exists') {
+     print "<html><head><title>Error!</title></head>\n";
+     print "$body\n";
+     print "<center><h1><font face=arial>Error!</font></h1></center>\n";
+     print "<font face=arial>The user you are trying to enter already exists. Please go back and enter a different username</font>\n";
+     print "</font></body></html>\n";
+   } elsif ($error eq 'ingroup') {
+     print "<html><head><title>Error!</title></head>\n";
+     print "$body\n";
+     print "<center><h1><font face=arial>Error!</font></h1></center>\n";
+     print "<font face=arial>This user is already in the group <i>$arg1</i>. Please go back and deselect group <i>$arg1</i> from the list.</font>\n";
+     print "<form><input type=button value=\"<<Back\" OnClick=\"history.back()\"></form>\n";
+     print "</font></body></html>\n";
+   } elsif ($error eq 'sess_expired') {
+     print "<html>$body\n";
+     print "<center><font size=4>Your session has expired.</font></center>\n";
+     print "<br><br><center>Please login again <a href=\"$cgi\?action=login\" target=\"_top\"> HERE</a></center>\n";
+     print "</body></html>\n";
+   } elsif ($error eq 'open') {
+     print "<html>$body\n";
+     print "<center><font size=4>Unable to open or rename file.</font></center>\n";
+     print "<br><br><center>If this continues, please contact your administrator</center>\n";
+     print "</body></html>\n";
+   }
+
+
+   exit;
+
+}
+
+
+#print a html header if not printed yet
+sub printheader {
+
+  if ($printheader) {
+     print "Content-Type: text/html\n\n";
+     $printheader = 0;
+  }
+
+}
+
+
+#verify user can access administration
+sub checksession {
+
+  my $username = $query->cookie(-name=>'username');
+  my $sessionid = $query->cookie(-name=>'ma_sessionid');
+
+  if ($sessionid eq '') {
+     printheader();
+     if ($query->param()) {
+        error('sess_expired');
+     } else {
+        printlogin();
+        exit;
+    }
+  }
+
+  my $now = time();
+  my $founduser = 0;
+  open(SESSFILE, "$sessionfile") || error('open');
+  error('open') if -l "$tmpdir/adminsess.$$";
+  open(NEWSESS, ">$tmpdir/adminsess.$$") || error('open');
+  while (<SESSFILE>) {
+       chomp();
+       my ($user, $sess, $time, $pkgnum, $svcdomain, $domname) = split(/\s+/);
+       next if $now - $sessexpire > $time;
+       if ($username eq $user && !$founduser) {
+               if ($sess eq $sessionid) {
+                       $founduser = 1;
+                       print NEWSESS "$user $sess $now $pkgnum $svcdomain $domname\n";
+                        $current_package=$pkgnum;
+                        $current_account=$svcdomain;
+                        $current_domname=$domname;
+                       next;
+               }
+       }
+       print NEWSESS "$user $sess $time $pkgnum $svcdomain $domname\n";
+  }
+  close(SESSFILE);
+  close(NEWSESS);
+  system("mv $tmpdir/adminsess.$$ $sessionfile");
+  error('sess_expired') unless $founduser;
+
+  my $cookie1 = $query->cookie(-name=>'username',
+                               -value=>$username,
+                               -expires=>$cookieexpire,
+                               -domain=>$cookiedomain);
+
+  my $cookie2 = $query->cookie(-name=>'ma_sessionid',
+                               -value=>$sessionid,
+                               -expires=>$cookieexpire,
+                               -domain=>$cookiedomain);
+
+  print $query->header(-COOKIE=>[$cookie1, $cookie2]);
+  
+  $printheader = 0;
+
+  return 0;
+
+}
+
+sub destroysession {
+
+  my $username = $query->cookie(-name=>'username');
+  my $sessionid = $query->cookie(-name=>'ma_sessionid');
+
+  if ($sessionid eq '') {
+     printheader();
+     if ($query->param()) {
+        error('sess_expired');
+     } else {
+        printlogin();
+        exit;
+    }
+  }
+
+  my $now = time();
+  my $founduser = 0;
+  open(SESSFILE, "$sessionfile") || error('open');
+  error('open') if -l "$tmpdir/adminsess.$$";
+  open(NEWSESS, ">$tmpdir/adminsess.$$") || error('open');
+  while (<SESSFILE>) {
+       chomp();
+       my ($user, $sess, $time, $pkgnum, $svcdomain, $domname) = split(/\s+/);
+       next if $now - $sessexpire > $time;
+       if ($username eq $user && !$founduser) {
+               if ($sess eq $sessionid) {
+                       $founduser = 1;
+                       next;
+               }
+       }
+       print NEWSESS "$user $sess $time $pkgnum $svcdomain $domname\n";
+  }
+  close(SESSFILE);
+  close(NEWSESS);
+  system("mv $tmpdir/adminsess.$$ $sessionfile");
+  error('sess_expired') unless $founduser;
+
+  $printheader = 0;
+
+  return 0;
+
+}
+
+# checks the username and pass against the database
+sub checkuserpass {
+
+  my $username = shift;
+  my $password = shift;
+
+  my $error = authenticate ( {
+      'authuser'         => $username,
+      '_password'        => $password,
+    } ); 
+
+  if ($error eq "$username OK") {
+    return 1;
+  }else{
+    return 0;
+  }
+
+}
+
+#printlogin prints a login page
+sub printlogin {
+
+        print "<html>$body\n";
+        print "<center><font size=4>Please login to access MAIL ADMIN</font></center>\n";
+        print "<form action=\"$cgi\" method=post>\n";
+        print "<center>Email Address: &nbsp; <input type=text name=\"username\">\n";
+        print "<br>Email Password: <input type=password name=\"password\">\n";
+        print "<br><input type=submit name=\"login\" value=\"Login\">\n";
+        print "</form></center>\n";
+        print "</body></html>\n";
+}
+
+
+#select_package chooses a administrable package if more than one exists
+sub select_package {
+        my $user = shift;
+        my $packages = list_packages($user);
+        if (scalar(@{$packages}) eq 1) {
+          $current_package = @{$packages}[0]->{'pkgnum'};
+          set_package();
+        }
+        if (scalar(@{$packages}) > 1) {
+#          print $query->redirect("$cgi\?action=list_packages");
+           print "<p>No package selected.  You must first <a href=\"$cgi\?action=list_packages\" target=\"rightmainframe\">select a package</a>.\n";
+          exit;
+        }
+}
+
+sub set_package {
+
+  my $username = $query->cookie(-name=>'username');
+  my $sessionid = $query->cookie(-name=>'ma_sessionid');
+
+  if ($sessionid eq '') {
+     printheader();
+     if ($query->param()) {
+        error('sess_expired');
+     } else {
+        printlogin();
+        exit;
+    }
+  }
+
+  my $now = time();
+  my $founduser = 0;
+  open(SESSFILE, "$sessionfile") || error('open');
+  error('open') if -l "$tmpdir/adminsess.$$";
+  open(NEWSESS, ">$tmpdir/adminsess.$$") || error('open');
+  while (<SESSFILE>) {
+       chomp();
+       my ($user, $sess, $time, $pkgnum, $svcdomain, $domname) = split(/\s+/);
+       next if $now - $sessexpire > $time;
+       if ($username eq $user && !$founduser) {
+               if ($sess eq $sessionid) {
+                       $founduser = 1;
+                       print NEWSESS "$user $sess $time $current_package $current_account $current_domname\n";
+                       next;
+               }
+       }
+       print NEWSESS "$user $sess $time $pkgnum $svcdomain $domname\n";
+  }
+  close(SESSFILE);
+  close(NEWSESS);
+  system("mv $tmpdir/adminsess.$$ $sessionfile");
+  error('sess_expired') unless $founduser;
+
+  $printheader = 0;
+
+  return 0;
+
+}
+
diff --git a/fs_selfadmin/FS-MailAdminServer/fs_mailadmind b/fs_selfadmin/FS-MailAdminServer/fs_mailadmind
new file mode 100755 (executable)
index 0000000..746d782
--- /dev/null
@@ -0,0 +1,366 @@
+#!/usr/bin/perl -Tw
+
+eval 'exec /usr/bin/perl -Tw -S $0 ${1+"$@"}'
+    if 0; # not running under some shell
+#
+# fs_mailadmind
+#
+# This is run REMOTELY over ssh by fs_mailadmin_server.
+#
+
+use strict;
+use Socket;
+
+use vars qw( $Debug );
+
+$Debug = 0;
+
+my($fs_mailadmind_socket)="/usr/local/freeside/fs_mailadmind_socket";
+
+$ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
+$ENV{'SHELL'} = '/bin/sh';
+$ENV{'IFS'} = " \t\n";
+$ENV{'CDPATH'} = '';
+$ENV{'ENV'} = '';
+$ENV{'BASH_ENV'} = '';
+
+$|=1;
+
+warn "[fs_mailadmind] Reading locales...\n" if $Debug;
+chomp( my $n_cust_main_county = <STDIN> );
+my @cust_main_county = map {
+  chomp( my $taxnum = <STDIN> );
+  chomp( my $state = <STDIN> );
+  chomp( my $county = <STDIN> );
+  chomp( my $country = <STDIN> );
+  {
+    'taxnum'  => $taxnum,
+    'state'   => $state,
+    'county'  => $county,
+    'country' => $country,
+  };
+} ( 1 .. $n_cust_main_county );
+
+warn "[fs_mailadmind] Reading package definitions...\n" if $Debug;
+chomp( my $n_part_pkg = <STDIN> );
+my @part_pkg = map {
+  chomp( my $pkgpart = <STDIN> );
+  chomp( my $pkg = <STDIN> );
+  {
+    'pkgpart' => $pkgpart,
+    'pkg'     => $pkg,
+  };
+} ( 1 .. $n_part_pkg );
+
+warn "[fs_mailadmind] Reading POPs...\n" if $Debug;
+chomp( my $n_svc_acct_pop = <STDIN> );
+my @svc_acct_pop = map {
+  chomp( my $popnum = <STDIN> );
+  chomp( my $city = <STDIN> );
+  chomp( my $state = <STDIN> );
+  chomp( my $ac = <STDIN> );
+  chomp( my $exch = <STDIN> );
+  chomp( my $loc = <STDIN> );
+  {
+    'popnum' => $popnum,
+    'city'   => $city,
+    'state'  => $state,
+    'ac'     => $ac,
+    'exch'   => $exch,
+    'loc'    => $loc,
+  };
+} ( 1 .. $n_svc_acct_pop );
+
+warn "[fs_mailadmind] Creating $fs_mailadmind_socket\n" if $Debug;
+my $uaddr = sockaddr_un($fs_mailadmind_socket);
+my $proto = getprotobyname('tcp');
+socket(Server,PF_UNIX,SOCK_STREAM,0) or die "socket: $!";
+unlink($fs_mailadmind_socket);
+bind(Server, $uaddr) or die "bind: $!";
+listen(Server,SOMAXCONN) or die "listen: $!";
+
+warn "[fs_mailadmind] Entering main loop...\n" if $Debug;
+my $paddr;
+for ( ; $paddr = accept(Client,Server); close Client) {
+
+  chop( my $command = <Client> );
+
+  if ( $command eq "signup_info" ) {
+    warn "[fs_mailadmind] sending signup info...\n" if $Debug; 
+    print Client join("\n", $n_cust_main_county,
+      map {
+        $_->{taxnum},
+        $_->{state},
+        $_->{county},
+        $_->{country},
+      } @cust_main_county
+    ), "\n";
+
+    print Client join("\n", $n_part_pkg,
+      map {
+        $_->{pkgpart},
+        $_->{pkg},
+      } @part_pkg
+    ), "\n";
+
+    print Client join("\n", $n_svc_acct_pop,
+      map {
+        $_->{popnum},
+        $_->{city},
+        $_->{state},
+        $_->{ac},
+        $_->{exch},
+        $_->{loc},
+      } @svc_acct_pop
+    ), "\n";
+
+  } elsif ( $command eq "new_customer" ) {
+    warn "[fs_mailadmind] reading customer signup...\n" if $Debug;
+    my(
+      $first, $last, $ss, $company, $address1, $address2, $city, $county,
+      $state, $zip, $country, $daytime, $night, $fax, $payby, $payinfo,
+      $paydate, $payname, $invoicing_list, $pkgpart, $username, $password,
+      $popnum,
+    ) = map { scalar(<Client>) } ( 1 .. 23 );
+
+    warn "[fs_mailadmind] sending customer data to remote server...\n" if $Debug;
+    print 
+      $first, $last, $ss, $company, $address1, $address2, $city, $county,
+      $state, $zip, $country, $daytime, $night, $fax, $payby, $payinfo,
+      $paydate, $payname, $invoicing_list, $pkgpart, $username, $password,
+      $popnum,
+    ;
+
+    warn "[fs_mailadmind] reading error from remote server...\n" if $Debug;
+    my $error = <STDIN>;
+
+    warn "[fs_mailadmind] sending error to local client...\n" if $Debug;
+    print Client $error;
+
+  } elsif ( $command eq "authenticate" ) {
+    warn "[fs_mailadmind] reading user information to auth...\n" if $Debug;
+    chop( my $user = <Client> );
+    warn "[fs_mailadmind] reading authentication material...\n" if $Debug;
+    chop( my $password = <Client> );
+    warn "[fs_mailadmind] sending information to remote server...\n" if $Debug;
+    print "authenticate\n", $user, "\n", $password, "\n";
+
+    warn "[fs_mailadmind] reading error from remote server...\n" if $Debug;
+    my $error = <STDIN>;
+
+    warn "[fs_mailadmind] sending error to local client...\n" if $Debug;
+    print Client $error;
+    
+  } elsif ( $command eq "list_packages" ) {
+    warn "[fs_mailadmind] reading user information to list_packages...\n" if $Debug;
+    chop( my $user = <Client> );
+    warn "[fs_mailadmind] sending user information to remote server...\n" if $Debug;
+    print "list_packages\n", $user, "\n";
+
+    warn "[fs_mailadmind] reading data from remote server...\n" if $Debug;
+    chomp( my $n_packages = <STDIN> );
+    my @packages = map {
+      chomp( my $pkgnum  = <STDIN> );
+      chomp( my $domain  = <STDIN> );
+      chomp( my $account = <STDIN> );
+      {
+        'pkgnum'  => $pkgnum,
+        'domain'  => $domain,
+        'account' => $account,
+      };
+    } ( 1 .. $n_packages );
+
+    warn "[fs_mailadmind] sending data to local client...\n" if $Debug;
+
+    print Client join("\n", $n_packages,
+      map {
+        $_->{pkgnum},
+        $_->{domain},
+        $_->{account},
+      } @packages
+    ), "\n";
+
+  } elsif ( $command eq "list_mailboxes" ) {
+    warn "[fs_mailadmind] reading user information to list_mailboxes...\n" if $Debug;
+    chop( my $user = <Client> );
+    warn "[fs_mailadmind] reading package number to list_mailboxes...\n" if $Debug;
+    chop( my $package = <Client> );
+    warn "[fs_mailadmind] sending user information to remote server...\n" if $Debug;
+    print "list_mailboxes\n", $user, "\n", $package, "\n";
+
+    warn "[fs_mailadmind] reading data from remote server...\n" if $Debug;
+    chomp( my $n_svc_acct = <STDIN> );
+    my @svc_acct = map {
+      chomp( my $svcnum = <STDIN> );
+      chomp( my $username = <STDIN> );
+      chomp( my $_password = <STDIN> );
+      {
+        'svcnum' => $svcnum,
+        'username' => $username,
+        '_password'     => $_password,
+      };
+    } ( 1 .. $n_svc_acct );
+
+    warn "[fs_mailadmind] sending data to local client...\n" if $Debug;
+
+    print Client join("\n", $n_svc_acct,
+      map {
+        $_->{svcnum},
+        $_->{username},
+        $_->{_password},
+      } @svc_acct
+    ), "\n";
+
+  } elsif ( $command eq "delete_mailbox" ) {
+    warn "[fs_mailadmind] reading user information to auth...\n" if $Debug;
+    chop( my $user = <Client> );
+    warn "[fs_mailadmind] reading account information to delete...\n" if $Debug;
+    chop( my $account = <Client> );
+    warn "[fs_mailadmind] sending information to remote server...\n" if $Debug;
+    print "delete_mailbox\n", $user, "\n", $account, "\n";
+
+    warn "[fs_mailadmind] reading error from remote server...\n" if $Debug;
+    my $error = <STDIN>;
+
+    warn "[fs_mailadmind] sending error to local client...\n" if $Debug;
+    print Client $error;
+
+  } elsif ( $command eq "password_mailbox" ) {
+    warn "[fs_mailadmind] reading user information to auth...\n" if $Debug;
+    chop( my $user = <Client> );
+    warn "[fs_mailadmind] reading account information to password...\n" if $Debug;
+    my(
+      $account, $_password,
+    ) = map { scalar(<Client>) } ( 1 .. 2 );
+
+    warn "[fs_mailadmind] sending password data to remote server...\n" if $Debug;
+    print "password_mailbox", "\n";
+    print 
+      $user, "\n", $account, $_password,
+    ;
+
+    warn "[fs_mailadmind] reading error from remote server...\n" if $Debug;
+    my $error = <STDIN>;
+
+    warn "[fs_mailadmind] sending error to local client...\n" if $Debug;
+    print Client $error;
+
+  } elsif ( $command eq "add_mailbox" ) {
+    warn "[fs_mailadmind] reading user information to auth...\n" if $Debug;
+    chop( my $user = <Client> );
+    warn "[fs_mailadmind] reading account information to create...\n" if $Debug;
+    my(
+      $package, $account, $_password,
+    ) = map { scalar(<Client>) } ( 1 .. 3 );
+
+    warn "[fs_mailadmind] sending service data to remote server...\n" if $Debug;
+    print "add_mailbox", "\n";
+    print 
+      $user, "\n", $package, $account, $_password,
+    ;
+
+    warn "[fs_mailadmind] reading error from remote server...\n" if $Debug;
+    my $error = <STDIN>;
+
+    warn "[fs_mailadmind] sending error to local client...\n" if $Debug;
+    print Client $error;
+
+  } elsif ( $command eq "add_forward" ) {
+    warn "[fs_mailadmind] reading user information to auth...\n" if $Debug;
+    chop( my $user = <Client> );
+    warn "[fs_mailadmind] reading forward information to create...\n" if $Debug;
+    my(
+      $package, $source, $dest,
+    ) = map { scalar(<Client>) } ( 1 .. 3 );
+
+    warn "[fs_mailadmind] sending service data to remote server...\n" if $Debug;
+    print "add_forward", "\n";
+    print 
+      $user, "\n", $package, $source, $dest,
+    ;
+
+    warn "[fs_mailadmind] reading error from remote server...\n" if $Debug;
+    my $error = <STDIN>;
+
+    warn "[fs_mailadmind] sending error to local client...\n" if $Debug;
+    print Client $error;
+
+  } elsif ( $command eq "delete_forward" ) {
+    warn "[fs_mailadmind] reading user information to auth...\n" if $Debug;
+    chop( my $user = <Client> );
+    warn "[fs_mailadmind] reading forward information to delete...\n" if $Debug;
+    chop( my $service = <Client> );
+    warn "[fs_mailadmind] sending information to remote server...\n" if $Debug;
+    print "delete_forward\n", $user, "\n", $service, "\n";
+
+    warn "[fs_mailadmind] reading error from remote server...\n" if $Debug;
+    my $error = <STDIN>;
+
+    warn "[fs_mailadmind] sending error to local client...\n" if $Debug;
+    print Client $error;
+
+  } elsif ( $command eq "list_forwards" ) {
+    warn "[fs_mailadmind] reading user information to list_forwards...\n" if $Debug;
+    chop( my $user = <Client> );
+    warn "[fs_mailadmind] reading service number to list_forwards...\n" if $Debug;
+    chop( my $service = <Client> );
+    warn "[fs_mailadmind] sending user information to remote server...\n" if $Debug;
+    print "list_forwards\n", $user, "\n", $service, "\n";
+
+    warn "[fs_mailadmind] reading data from remote server...\n" if $Debug;
+    chomp( my $n_svc_forward = <STDIN> );
+    my @svc_forward = map {
+      chomp( my $svcnum = <STDIN> );
+      chomp( my $dest = <STDIN> );
+      {
+        'svcnum' => $svcnum,
+        'dest' => $dest,
+      };
+    } ( 1 .. $n_svc_forward );
+
+    warn "[fs_mailadmind] sending data to local client...\n" if $Debug;
+
+    print Client join("\n", $n_svc_forward,
+      map {
+        $_->{svcnum},
+        $_->{dest},
+      } @svc_forward
+    ), "\n";
+
+  } elsif ( $command eq "list_pkg_forwards" ) {
+    warn "[fs_mailadmind] reading user information to list_pkg_forwards...\n" if $Debug;
+    chop( my $user = <Client> );
+    warn "[fs_mailadmind] reading service number to list_forwards...\n" if $Debug;
+    chop( my $package = <Client> );
+    warn "[fs_mailadmind] sending user information to remote server...\n" if $Debug;
+    print "list_pkg_forwards\n", $user, "\n", $package, "\n";
+
+    warn "[fs_mailadmind] reading data from remote server...\n" if $Debug;
+    chomp( my $n_svc_forward = <STDIN> );
+    my @svc_forward = map {
+      chomp( my $svcnum = <STDIN> );
+      chomp( my $srcsvc = <STDIN> );
+      chomp( my $dest = <STDIN> );
+      {
+        'svcnum' => $svcnum,
+        'srcsvc' => $srcsvc,
+        'dest' => $dest,
+      };
+    } ( 1 .. $n_svc_forward );
+
+    warn "[fs_mailadmind] sending data to local client...\n" if $Debug;
+
+    print Client join("\n", $n_svc_forward,
+      map {
+        $_->{svcnum},
+        $_->{srcsvc},
+        $_->{dest},
+      } @svc_forward
+    ), "\n";
+
+  } else {
+    die "unexpected command from client: $command";
+  }
+
+}
+
diff --git a/fs_selfadmin/README b/fs_selfadmin/README
new file mode 100644 (file)
index 0000000..d9857f0
--- /dev/null
@@ -0,0 +1,27 @@
+
+This collection of files implements a 'self-administered mail service.'
+Configuration is similar to fs_signupd
+
+Additionally you will need to modify the database:
+
+CREATE TABLE svc_acct_admin (
+  svcnum int primary key,
+  adminsvc int not null
+);
+
+creating both as keys might be good
+
+(and perform the dbdef-create)
+
+
+As it exists now, a package containing one svc_domain, at least one
+svc_acct_admin, and other services can have its svc_acct's and svc_forward's
+manipulated by the svc_acct referenced by a svc_acct_admin in the package.
+
+One svc_acct may be referenced as svc_acct_admin for multiple packages.
+
+fs_mailadmin_server contains hard coded references to service numbers which
+will require editing for your system.
+
+It's not a lot, but it might provide inspiration.
+
diff --git a/fs_selfadmin/fs_mailadmin_server b/fs_selfadmin/fs_mailadmin_server
new file mode 100755 (executable)
index 0000000..ef47885
--- /dev/null
@@ -0,0 +1,642 @@
+#!/usr/bin/perl -Tw
+#
+# fs_mailadmin_server
+#
+
+use strict;
+use IO::Handle;
+use FS::SSH qw(sshopen2);
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_main_county;
+use FS::cust_main;
+use FS::svc_acct_admin;
+
+use vars qw( $opt $Debug $conf $default_domain );
+
+$Debug = 1;
+
+#my @payby = qw(CARD PREPAY);
+
+my $user = shift or die &usage;
+&adminsuidsetup( $user ); 
+
+$conf = new FS::Conf;
+$default_domain = $conf->config('domain');
+
+my $machine = shift or die &usage;
+
+my $agentnum = shift or die &usage;
+my $agent = qsearchs( 'agent', { 'agentnum' => $agentnum } ) or die &usage;
+my $pkgpart = $agent->pkgpart_hashref;
+
+my $refnum = shift or die &usage;
+
+#causing trouble for some folks
+#$SIG{CHLD} = sub { wait() };
+
+my($fs_mailadmind)=$conf->config('fs_mailadmind');
+
+while (1) {
+  my($reader,$writer)=(new IO::Handle, new IO::Handle);
+  $writer->autoflush(1);
+  warn "[fs_mailadmin_server] Connecting to $machine...\n" if $Debug;
+  sshopen2($machine,$reader,$writer,$fs_mailadmind);
+
+  my $data;
+
+  warn "[fs_mailadmin_server] Sending locales...\n" if $Debug;
+  my @cust_main_county = qsearch('cust_main_county', {} );
+  print $writer $data = join("\n",
+    ( scalar(@cust_main_county) || die "no tax rates (cust_main_county records)" ),
+    map {
+      $_->taxnum,
+      $_->state,
+      $_->county,
+      $_->country,
+    } @cust_main_county
+  ),"\n";
+  warn "[fs_mailadmin_server] $data\n" if $Debug > 2;
+
+  warn "[fs_mailadmin_server] Sending package definitions...\n" if $Debug;
+  my @part_pkg = grep { $_->svcpart('svc_acct') && $pkgpart->{ $_->pkgpart } }
+    qsearch( 'part_pkg', {} );
+  print $writer $data = join("\n",
+    ( scalar(@part_pkg) || die "no usable package definitions, agent $agentnum" ),
+    map {
+      $_->pkgpart,
+      $_->pkg,
+    } @part_pkg
+  ), "\n";
+  warn "[fs_mailadmin_server] $data\n" if $Debug > 2;
+
+  warn "[fs_mailadmin_server] Sending POPs...\n" if $Debug;
+  my @svc_acct_pop = qsearch ('svc_acct_pop',{} );
+  print $writer $data = join("\n",
+    ( scalar(@svc_acct_pop) || die "No points of presence (svc_acct_pop records)" ),
+    map {
+      $_->popnum,
+      $_->city,
+      $_->state,
+      $_->ac,
+      $_->exch,
+      $_->loc,
+    } @svc_acct_pop
+  ), "\n";
+  warn "[fs_mailadmin_server] $data\n" if $Debug > 2;
+
+  warn "[fs_mailadmin_server] Entering main loop...\n" if $Debug;
+COMMAND:  while (1) {
+    warn "[fs_mailadmin_server] Reading (waiting for) command...\n" if $Debug;
+    chop( my($command, $user) = map { scalar(<$reader>) } ( 1 .. 2 ) );
+    my $domain = $default_domain;
+    $user =~ /^([\w\.\-]+)\@(([\w\-]+\.)+\w+)$/;
+    ($user, $domain) = ($1, $2);
+
+    if ($command eq 'authenticate'){
+      warn "[fs_mailadmin_server] Processing authenticate command for $user \n" if $Debug;
+      chop( my($password) = map { scalar(<$reader>) } ( 1 .. 1 ) );
+
+      my $error = '';
+
+      my @svc_domain = qsearchs('svc_domain', { 'domain'   => $domain });
+
+      if (scalar(@svc_domain) != 1) {
+        warn "Nonexistant or duplicate service account for \"$domain\"";
+        next COMMAND;
+      }
+
+      my @svc_acct = qsearchs('svc_acct', { 'username' => $user,
+                                            'domsvc'   => $svc_domain[0]->svcnum });
+      if (scalar(@svc_acct) != 1) {
+        die "Nonexistant or duplicate service account for \"$user\"";
+        next COMMAND;
+      }
+
+      if ($svc_acct[0]->_password eq $password) {
+        $error = "$user\@$domain OK";
+      }else{
+        $error = "$user\@$domain FAILED";
+      }
+      warn "[fs_mailadmin_server] Sending results...\n" if $Debug;
+      print $writer $error, "\n";
+    }
+    elsif ($command eq 'list_packages'){
+      warn "[fs_mailadmin_server] Processing list_packages command for $user \n" if $Debug;
+
+      my $error = '';
+
+      my @packages = eval {find_administrable_packages( $user, $domain )};
+      warn "$@" if $@; 
+
+      my %packages;
+      my %accounts;
+
+      foreach my $package (@packages) {
+        $packages{my $pkgnum = $package->getfield('pkgnum')} = $default_domain;
+        $accounts{$pkgnum} = 0;
+        my @services = qsearch('cust_svc', { 'pkgnum' => $pkgnum });
+        foreach my $service (@services) {
+          if ($service->getfield('svcpart') eq '4'){
+            my $account=qsearchs('svc_domain', { 'svcnum' => $service->getfield('svcnum') });
+            $packages{$pkgnum}=$account->getfield('domain');
+            $accounts{$pkgnum}=$account->getfield('svcnum');
+          }
+        }
+      }
+      
+      print $writer $data = join("\n",
+        ( scalar(keys(%packages)) ),
+        map {
+          $_,
+          $packages{$_},
+          $accounts{$_},
+        } keys(%packages)
+      ), "\n";
+      warn "[fs_mailadmin_server] $data\n" if $Debug > 2;
+
+    }elsif ($command eq 'list_mailboxes'){
+
+      warn "[fs_mailadmin_server] Processing list_mailboxes command for $user" if $Debug;
+      chop( my($pkgnum) = map { scalar(<$reader>) } ( 1 .. 1 ) );
+      warn "package $pkgnum \n" if $Debug;
+
+      my $error = '';
+
+      my @packages = eval {find_administrable_packages( $user, $domain )};
+      warn "$@" if $@; 
+
+      my @accounts;
+
+      foreach my $package (@packages) {
+        next unless ($pkgnum eq $package->getfield('pkgnum'));
+        my @services = qsearch('cust_svc', { 'pkgnum' => $package->getfield('pkgnum') });
+        foreach my $service (@services) {
+          if ($service->getfield('svcpart') eq '2'){
+            my $account=qsearchs('svc_acct', { 'svcnum' => $service->getfield('svcnum') });
+#           $accounts[$#accounts+1]=$account->getfield('username');
+            $accounts[$#accounts+1]=$account;
+          }
+        }
+      }
+      
+      print $writer $data = join("\n",
+#        ( scalar(@accounts) || die "No accounts (svc_acct records)" ),
+        ( scalar(@accounts) ),
+        map {
+          $_->svcnum,
+#          $_->username,
+          $_->email,
+#          $_->_password,
+          '*****',
+        } @accounts
+      ), "\n";
+      warn "[fs_mailadmin_server] $data\n" if $Debug > 2;
+
+      
+    } elsif ($command eq 'delete_mailbox'){
+      warn "[fs_mailadmin_server] Processing delete_mailbox command for $user " if $Debug;
+      chop( my($account) = map { scalar(<$reader>) } ( 1 .. 1 ) );
+      warn "account $account \n" if $Debug;
+
+      my $error = '';
+
+      my @packages = eval { find_administrable_packages($user, $domain) };
+      warn "$@" if $@; 
+      $error ||= "$@" if $@; 
+
+      my @svc_acct = qsearchs('svc_acct', { 'svcnum' => $account }) unless $error;
+      if (scalar(@svc_acct) != 1) { $error ||= 'Nonexistant or duplicate service account for user.' };
+      if (! $error && check_administrator(\@packages, $svc_acct[0])){
+# not sure about the next three lines... do we delete? or return error
+        foreach my $svc_forward (qsearch('svc_forward', { 'dstsvc' => $svc_acct[0]->getfield('svcnum') })) {
+          $error ||= $svc_forward->delete;
+        }
+        foreach my $svc_forward (qsearch('svc_forward', { 'srcsvc' => $svc_acct[0]->getfield('svcnum') })) {
+          $error ||= $svc_forward->delete;
+        }
+        $error ||= $svc_acct[0]->delete;
+      } else {
+        $error ||= "Illegal attempt to remove service";
+      }
+
+      
+      warn "[fs_mailadmin_server] Sending results...\n" if $Debug;
+      print $writer $error, "\n";
+      
+    } elsif ($command eq 'password_mailbox'){
+      warn "[fs_mailadmin_server] Processing password_mailbox command for $user " if $Debug;
+      chop( my($account, $_password) = map { scalar(<$reader>) } ( 1 .. 2 ) );
+      warn "account $account with password $_password \n" if $Debug;
+
+      my $error = '';
+
+      my @packages = eval { find_administrable_packages($user, $domain) };
+      warn "$@" if $@; 
+      $error ||= "$@" if $@; 
+
+      my @svc_acct = qsearchs('svc_acct', { 'svcnum' => $account }) unless $error;
+      if (scalar(@svc_acct) != 1) { $error ||= 'Nonexistant or duplicate service account.' };
+
+      if (! $error && check_administrator(\@packages, $svc_acct[0])){
+        my $new = new FS::svc_acct ({$svc_acct[0]->hash});
+        $new->setfield('_password' => $_password);
+        $error ||= $new->replace($svc_acct[0]);
+      } else {
+        $error ||= "Illegal attempt to change password";
+      }
+
+      
+      warn "[fs_mailadmin_server] Sending results...\n" if $Debug;
+      print $writer $error, "\n";
+      
+    } elsif ($command eq 'add_mailbox'){
+      warn "[fs_mailadmin_server] Processing add_mailbox command for $user " if $Debug;
+      chop( my($target_package, $account, $_password) = map { scalar(<$reader>) } ( 1 .. 3 ) );
+      warn "in package $target_package account $account with password $_password \n" if $Debug;
+
+      my $found_package;
+      my $domainsvc=0;
+      my $svcpart=2;    # this is 'email box'
+      my $svcpartsm=3;  # this is 'domain alias'
+      my $error = '';
+      my $found = 0;
+
+      my @packages = eval { find_administrable_packages($user, $domain) };
+      warn "$@" if $@; 
+      $error ||= "$@" if $@; 
+
+      foreach my $package (@packages) {
+        if ($package->getfield('pkgnum') eq $target_package) {
+          $found = 1;
+          $found_package=$package;
+          my @services = qsearch('cust_svc', { 'pkgnum' => $target_package });
+          foreach my $service (@services) {
+            if ($service->getfield('svcpart') eq '4'){
+              my @svc_domain=qsearchs('svc_domain', { 'svcnum' => $service->getfield('svcnum') });
+              if (scalar(@svc_domain) eq 1) {
+                $domainsvc=$svc_domain[0]->getfield('svcnum');
+              }
+            }
+          }
+          last;
+        }
+      }
+      warn "User $user does not have administration rights to package $target_package\n" unless $found;
+      $error ||= "User $user does not have administration rights to package $target_package\n" unless $found;
+
+      my $part_pkg = qsearchs('part_pkg',{'pkgpart'=>$found_package->getfield('pkgpart')});
+
+      #list of services this pkgpart includes (although at the moment we only care
+      #  about $svcpart
+      my $pkg_svc;
+      my %pkg_svc = ();
+      foreach $pkg_svc ( qsearch('pkg_svc',{'pkgpart'=> $found_package->pkgpart }) ) {
+        $pkg_svc{$pkg_svc->svcpart} = $pkg_svc->quantity if $pkg_svc->quantity;
+      }
+
+      my @services = qsearch('cust_svc', {'pkgnum'  => $found_package->getfield('pkgnum'),
+                                          'svcpart' => $svcpart,
+                                         });
+
+      if (scalar(@services) >= $pkg_svc{$svcpart}) {
+        $error="Maximum allowed already reached.";
+      }
+      
+      my $svc_acct = new FS::svc_acct ( {
+        'pkgnum'    => $found_package->pkgnum,
+        'svcpart'   => $svcpart,
+        'username'  => $account,
+        'domsvc'    => $domainsvc,
+        '_password' => $_password,
+      } );
+
+      my $y = $svc_acct->setdefault; # arguably should be in new method
+      $error ||= $y unless ref($y);
+      #and just in case you were silly
+      $svc_acct->pkgnum($found_package->pkgnum);
+      $svc_acct->svcpart($svcpart);
+      $svc_acct->username($account);
+      $svc_acct->domsvc($domainsvc);
+      $svc_acct->_password($_password);
+
+      $error ||= $svc_acct->check;
+
+      if ( ! $error ) { #in this case, $cust_pkg should always
+                                     #be definied, but....
+        $error ||= $svc_acct->insert;
+        warn "WARNING: $error on pre-checked svc_acct record!" if $error;
+      }
+
+      warn "[fs_mailadmin_server] Sending results...\n" if $Debug;
+      print $writer $error, "\n";
+      
+    }elsif ($command eq 'list_forwards'){
+
+      warn "[fs_mailadmin_server] Processing list_forwards command for $user" if $Debug;
+      chop( my($svcnum) = map { scalar(<$reader>) } ( 1 .. 1 ) );
+      warn "service $svcnum \n" if $Debug;
+
+      my $error = '';
+
+      my @packages = eval {find_administrable_packages( $user, $domain )};
+      warn "$@" if $@; 
+
+      my @forwards;
+
+      foreach my $package (@packages) {
+#        next unless ($pkgnum eq $package->getfield('pkgnum'));
+        my @services = qsearch('cust_svc', { 'pkgnum' => $package->getfield('pkgnum') });
+        foreach my $service (@services) {
+          if ($service->getfield('svcpart') eq '10'){
+            my $forward=qsearchs('svc_forward', { 'svcnum' => $service->getfield('svcnum') });
+            $forwards[$#forwards+1]=$forward if ($forward->getfield('srcsvc') == $svcnum);
+          }
+        }
+      }
+      
+      print $writer $data = join("\n",
+        ( scalar(@forwards) ),
+        map {
+          $_->svcnum,
+          ($_->dstsvc ? qsearchs('svc_acct', {'svcnum' => $_->dstsvc})->email : $_->dst),
+        } @forwards
+      ), "\n";
+      warn "[fs_mailadmin_server] $data\n" if $Debug > 2;
+
+      
+    }elsif ($command eq 'list_pkg_forwards'){
+
+      warn "[fs_mailadmin_server] Processing list_pkg_forwards command for $user" if $Debug;
+      chop( my($pkgnum) = map { scalar(<$reader>) } ( 1 .. 1 ) );
+      warn "package $pkgnum \n" if $Debug;
+
+      my $error = '';
+
+      my @packages = eval {find_administrable_packages( $user, $domain )};
+      warn "$@" if $@; 
+
+      my @forwards;
+
+      foreach my $package (@packages) {
+        next unless ($pkgnum eq $package->getfield('pkgnum'));
+        my @services = qsearch('cust_svc', { 'pkgnum' => $package->getfield('pkgnum') });
+        foreach my $service (@services) {
+          if ($service->getfield('svcpart') eq '10'){
+            my $forward=qsearchs('svc_forward', { 'svcnum' => $service->getfield('svcnum') });
+            $forwards[$#forwards+1]=$forward;
+          }
+        }
+      }
+      
+      print $writer $data = join("\n",
+        ( scalar(@forwards) ),
+        map {
+          $_->svcnum,
+          $_->srcsvc,
+          ($_->dstsvc ? qsearchs('svc_acct', {'svcnum' => $_->dstsvc})->email : $_->dst),
+        } @forwards
+      ), "\n";
+      warn "[fs_mailadmin_server] $data\n" if $Debug > 2;
+
+      
+    } elsif ($command eq 'delete_forward'){
+      warn "[fs_mailadmin_server] Processing delete_forward command for $user " if $Debug;
+      chop( my($forward) = map { scalar(<$reader>) } ( 1 .. 1 ) );
+      warn "forward $forward \n" if $Debug;
+
+      my $error = '';
+
+      my @packages = eval { find_administrable_packages($user, $domain) };
+      warn "$@" if $@; 
+      $error ||= "$@" if $@; 
+
+      my @svc_forward = qsearchs('svc_forward', { 'svcnum' => $forward }) unless $error;
+      if (scalar(@svc_forward) != 1) { $error ||= 'Nonexistant or duplicate service account for user.' };
+      if (! $error && check_administrator(\@packages, $svc_forward[0])){
+# not sure about the next three lines... do we delete? or return error
+        $error ||= $svc_forward[0]->delete;
+      } else {
+        $error ||= "Illegal attempt to remove service";
+      }
+
+      
+      warn "[fs_mailadmin_server] Sending results...\n" if $Debug;
+      print $writer $error, "\n";
+      
+    } elsif ($command eq 'add_forward'){
+      warn "[fs_mailadmin_server] Processing add_forward command for $user " if $Debug;
+      chop( my($target_package, $source, $dest) = map { scalar(<$reader>) } ( 1 .. 3 ) );
+      warn "in package $target_package source $source with destination $dest \n" if $Debug;
+
+      my $found_package;
+      my $domainsvc=0;
+      my $svcpart=10;   # this is 'forward service'
+      my $error = '';
+      my $found = 0;
+
+      my @packages = eval { find_administrable_packages($user, $domain) };
+      warn "$@" if $@; 
+      $error ||= "$@" if $@; 
+
+      foreach my $package (@packages) {
+        if ($package->getfield('pkgnum') eq $target_package) {
+          $found = 1;
+          $found_package=$package;
+          last;
+        }
+      }
+      warn "User $user does not have administration rights to package $target_package\n" unless $found;
+      $error ||= "User $user does not have administration rights to package $target_package\n" unless $found;
+
+      my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $source });
+      warn "Forwarding source $source does not exist.\n" unless $svc_acct;
+      $error ||= "Forwarding source $source does not exist.\n" unless $svc_acct;
+
+      my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $source });
+      warn "Forwarding source $source not attached to any account.\n" unless $cust_svc;
+      $error ||= "Forwarding source $source not attached to any account.\n" unless $cust_svc;
+
+      if ( ! $error ) {
+        warn "Forwarding source $source is not in package $target_package\n"
+          unless ($cust_svc->getfield('pkgnum') == $target_package);
+        $error ||= "Forwarding source $source is not in package $target_package\n"
+          unless ($cust_svc->getfield('pkgnum') == $target_package);
+      }
+
+      my $part_pkg = qsearchs('part_pkg',{'pkgpart'=>$found_package->getfield('pkgpart')});
+
+      #list of services this pkgpart includes (although at the moment we only care
+      #  about $svcpart
+      my $pkg_svc;
+      my %pkg_svc = ();
+      foreach $pkg_svc ( qsearch('pkg_svc',{'pkgpart'=> $found_package->pkgpart }) ) {
+        $pkg_svc{$pkg_svc->svcpart} = $pkg_svc->quantity if $pkg_svc->quantity;
+      }
+
+      my @services = qsearch('cust_svc', {'pkgnum'  => $found_package->getfield('pkgnum'),
+                                          'svcpart' => $svcpart,
+                                         });
+
+      if (scalar(@services) >= $pkg_svc{$svcpart}) {
+        $error="Maximum allowed already reached.";
+      }
+      
+      my $svc_forward = new FS::svc_forward ( {
+        'pkgnum'    => $found_package->pkgnum,
+        'svcpart'   => $svcpart,
+        'srcsvc'  => $source,
+        'dstsvc'    => 0,
+        'dst' => $dest,
+      } );
+
+      my $y = $svc_forward->setdefault; # arguably should be in new method
+      $error ||= $y unless ref($y);
+      #and just in case you were silly
+      $svc_forward->pkgnum($found_package->pkgnum);
+      $svc_forward->svcpart($svcpart);
+      $svc_forward->srcsvc($source);
+      $svc_forward->dstsvc(0);
+      $svc_forward->dst($dest);
+
+      $error ||= $svc_forward->check;
+
+      if ( ! $error ) { #in this case, $cust_pkg should always
+                                     #be definied, but....
+        $error ||= $svc_forward->insert;
+        warn "WARNING: $error on pre-checked svc_forward record!" if $error;
+      }
+
+      warn "[fs_mailadmin_server] Sending results...\n" if $Debug;
+      print $writer $error, "\n";
+      
+    } else {
+      warn "[fs_mailadmin_server] Bad command: $command \n" if $Debug;
+      print $writer "Bad command \n";
+    }
+  }
+  close $writer;
+  close $reader;
+  warn "connection to $machine lost!  waiting 60 seconds...\n";
+  sleep 60;
+  warn "reconnecting...\n";
+}
+
+sub usage {
+  die "Usage:\n\n  fs_mailadmin_server user machine agentnum refnum\n";
+}
+
+#sub find_administrable_packages {
+#      my $user = shift;
+#
+#      my $error = '';
+#
+#      my @svc_acct = qsearchs('svc_acct', { 'username' => $user });
+#      if (scalar(@svc_acct) != 1) {
+#        die "Nonexistant or duplicate service account for \"$user\"";
+#      }
+#
+#      my @cust_svc = qsearchs('cust_svc', { 'svcnum' => $svc_acct[0]->getfield('svcnum') });
+#      if (scalar(@cust_svc) != 1 ) {
+#        die "Nonexistant or duplicate customer service for \"$user\"";
+#      }
+#
+#      my @cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $cust_svc[0]->getfield('pkgnum') });
+#      if (scalar(@cust_pkg) != 1) {
+#        die "Nonexistant or duplicate customer package for \"$user\"";
+#      }
+#
+#      my @cust_main = qsearchs('cust_main', { 'custnum' => $cust_pkg[0]->getfield('custnum') });
+#      if (scalar(@cust_main) != 1 ) {
+#        die "Nonexistant or duplicate customer for \"$user\"";
+#      }
+#
+#      my @packages = $cust_main[0]->ncancelled_pkgs;
+#}
+
+sub find_administrable_packages {
+      my $user = shift;
+      my $domain = shift;
+
+      my @packages;
+      my $error = '';
+
+      my @svc_domain = qsearchs('svc_domain', { 'domain'   => $domain });
+
+      if (scalar(@svc_domain) != 1) {
+        die "Nonexistant or duplicate service account for \"$domain\"";
+      }
+
+      my @svc_acct = qsearchs('svc_acct', { 'username' => $user,
+                                            'domsvc'   => $svc_domain[0]->svcnum });
+      if (scalar(@svc_acct) != 1) {
+        die "Nonexistant or duplicate service account for \"$user\"";
+      }
+
+      my @svc_acct_admin = qsearch('svc_acct_admin', {'adminsvc' => $svc_acct[0]->getfield('svcnum') });
+      die "Nonexistant or duplicate customer service for \"$user\"" unless scalar(@svc_acct_admin);
+
+      foreach my $svc_acct_admin (@svc_acct_admin) {
+        my @cust_svc = qsearchs('cust_svc', { 'svcnum' => $svc_acct_admin->getfield('svcnum') });
+        if (scalar(@cust_svc) != 1 ) {
+          die "Nonexistant or duplicate customer service for admin \"$svc_acct_admin->getfield('svcnum')\"";
+        }
+
+        my @cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $cust_svc[0]->getfield('pkgnum') });
+        if (scalar(@cust_pkg) != 1) {
+          die "Nonexistant or duplicate customer package for admin \"$user\"";
+        }
+
+        push @packages, $cust_pkg[0] unless $cust_pkg[0]->getfield('cancel');
+
+      }
+      (@packages);
+}
+
+sub check_administrator {
+      my ($allowed_packages_aref, $svc_acct_ref) = @_;
+
+      my $error = '';
+      my $found = 0;
+
+      {
+        my @cust_svc = qsearchs('cust_svc', { 'svcnum' => $svc_acct_ref->getfield('svcnum') });
+        if (scalar(@cust_svc) != 1 ) {
+          warn "Nonexistant or duplicate customer service for \"$svc_acct_ref->getfield('username')\"";
+          last;
+        }
+
+        my @cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $cust_svc[0]->getfield('pkgnum') });
+        if (scalar(@cust_pkg) != 1) {
+          warn "Nonexistant or duplicate customer package for \"$svc_acct_ref->getfield('username')\"";
+          last;
+        }
+
+        foreach my $package (@$allowed_packages_aref) {
+          if ($package->getfield('pkgnum') eq $cust_pkg[0]->getfield('pkgnum')) {
+            $found = 1;
+            last;
+          }
+        }
+      }
+
+      $found;
+}
+
+sub check_add {
+      my ($allowed_packages_aref, $target_package) = @_;
+
+      my $error = '';
+      my $found = 0;
+
+      foreach my $package (@$allowed_packages_aref) {
+        if ($package->getfield('pkgnum') eq $target_package) {
+          $found = 1;
+          last;
+        }
+      }
+
+      $found;
+}
+
diff --git a/fs_selfservice/DEPLOY b/fs_selfservice/DEPLOY
new file mode 100755 (executable)
index 0000000..4aef4cf
--- /dev/null
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+kill `cat /var/run/freeside-selfservice-server.fs_selfservice.pid`
+
+( cd ..; make deploy; cd fs_selfservice )
+
+cd FS-SelfService
+perl Makefile.PL && make && make install
+
+cp /home/ivan/freeside/fs_selfservice/FS-SelfService/cgi/* /var/www/MyAccount
+chown freeside /var/www/MyAccount/selfservice.cgi
+chmod 755 /var/www/MyAccount/selfservice.cgi
+ln -s /var/www/MyAccount/selfservice.cgi /var/www/MyAccount/index.cgi || true
diff --git a/fs_selfservice/FS-SelfService/Changes b/fs_selfservice/FS-SelfService/Changes
new file mode 100644 (file)
index 0000000..b9e26b7
--- /dev/null
@@ -0,0 +1,6 @@
+Revision history for Perl extension FS::SelfService.
+
+0.01  Tue May 28 16:49:41 2002
+       - original version; created by h2xs 1.21 with options
+               -A -X -n FS::SelfService
+
diff --git a/fs_selfservice/FS-SelfService/MANIFEST b/fs_selfservice/FS-SelfService/MANIFEST
new file mode 100644 (file)
index 0000000..ebd0d3b
--- /dev/null
@@ -0,0 +1,6 @@
+Changes
+Makefile.PL
+MANIFEST
+SelfService.pm
+test.pl
+freeside-selfservice-clientd
diff --git a/fs_selfservice/FS-SelfService/Makefile.PL b/fs_selfservice/FS-SelfService/Makefile.PL
new file mode 100644 (file)
index 0000000..da0a0aa
--- /dev/null
@@ -0,0 +1,15 @@
+use ExtUtils::MakeMaker;
+# See lib/ExtUtils/MakeMaker.pm for details of how to influence
+# the contents of the Makefile that is written.
+WriteMakefile(
+    'NAME'             => 'FS::SelfService',
+    'VERSION_FROM'     => 'SelfService.pm', # finds $VERSION
+    'EXE_FILES'         => [ 'freeside-selfservice-clientd' ],
+    'INSTALLSCRIPT'     => '/usr/local/sbin',
+    'INSTALLSITEBIN'    => '/usr/local/sbin',
+    'PERM_RWX'          => '750',
+    'PREREQ_PM'                => {}, # e.g., Module::Name => 1.1
+    ($] >= 5.005 ?    ## Add these new keywords supported since 5.005
+      (ABSTRACT_FROM => 'SelfService.pm', # retrieve abstract from module
+       AUTHOR     => 'Ivan Kohler <ivan-freeside-selfservice@420.am>') : ()),
+);
diff --git a/fs_selfservice/FS-SelfService/SelfService.pm b/fs_selfservice/FS-SelfService/SelfService.pm
new file mode 100644 (file)
index 0000000..5849b28
--- /dev/null
@@ -0,0 +1,115 @@
+package FS::SelfService;
+
+use strict;
+use vars qw($VERSION @ISA @EXPORT_OK $socket %autoload );
+use Exporter;
+use Socket;
+use FileHandle;
+#use IO::Handle;
+use IO::Select;
+use Storable qw(nstore_fd fd_retrieve);
+
+$VERSION = '0.03';
+
+@ISA = qw( Exporter );
+
+$socket =  "/usr/local/freeside/selfservice_socket";
+
+%autoload = (
+  'passwd'          => 'passwd/passwd',
+  'chfn'            => 'passwd/passwd',
+  'chsh'            => 'passwd/passwd',
+  'login'           => 'MyAccount/login',
+  'customer_info'   => 'MyAccount/customer_info',
+  'invoice'         => 'MyAccount/invoice',
+  'cancel'          => 'MyAccount/cancel',
+  'payment_info'    => 'MyAccount/payment_info',
+  'process_payment' => 'MyAccount/process_payment',
+);
+@EXPORT_OK = keys %autoload;
+
+$ENV{'PATH'} ='/usr/bin:/usr/ucb:/bin';
+$ENV{'SHELL'} = '/bin/sh';
+$ENV{'IFS'} = " \t\n";
+$ENV{'CDPATH'} = '';
+$ENV{'ENV'} = '';
+$ENV{'BASH_ENV'} = '';
+
+my $freeside_uid = scalar(getpwnam('freeside'));
+die "not running as the freeside user\n" if $> != $freeside_uid;
+
+=head1 NAME
+
+FS::SelfService - Freeside self-service API
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+Use this API to implement your own client "self-service" module.
+
+If you just want to customize the look of the existing "self-service" module,
+see XXXX instead.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item passwd
+
+Returns the empty value on success, or an error message on errors.
+
+=cut
+
+foreach my $autoload ( keys %autoload ) {
+
+  my $eval =
+  "sub $autoload { ". '
+                   my $param;
+                   if ( ref($_[0]) ) {
+                     $param = shift;
+                   } else {
+                     $param = { @_ };
+                   }
+
+                   $param->{_packet} = \''. $autoload{$autoload}. '\';
+
+                   simple_packet($param);
+                 }';
+
+  eval $eval;
+  die $@ if $@;
+
+}
+
+sub simple_packet {
+  my $packet = shift;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($socket)) or die "connect: $!";
+  nstore_fd($packet, \*SOCK) or die "can't send packet: $!";
+  SOCK->flush;
+
+  #shoudl trap: Magic number checking on storable file failed at blib/lib/Storable.pm (autosplit into blib/lib/auto/Storable/fd_retrieve.al) line 337, at /usr/local/share/perl/5.6.1/FS/SelfService.pm line 71
+
+  #block until there is a message on socket
+#  my $w = new IO::Select;
+#  $w->add(\*SOCK);
+#  my @wait = $w->can_read;
+  my $return = fd_retrieve(\*SOCK) or die "error reading result: $!";
+  die $return->{'_error'} if defined $return->{_error} && $return->{_error};
+
+  $return;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<freeside-selfservice-clientd>, L<freeside-selfservice-server>
+
+=cut
+
+1;
+
diff --git a/fs_selfservice/FS-SelfService/cgi/login.html b/fs_selfservice/FS-SelfService/cgi/login.html
new file mode 100644 (file)
index 0000000..ca6251e
--- /dev/null
@@ -0,0 +1,32 @@
+<HTML><HEAD><TITLE>Login</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=5>Login</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+<FORM ACTION="<%= $self_url %>" METHOD=POST>
+<INPUT TYPE="hidden" NAME="session" VALUE="login">
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=2 CELLPADDING=0>
+<TR>
+  <TH ALIGN="right">Username </TH>
+  <TD>
+    <!-- <INPUT TYPE="text" NAME="username" VALUE="<%= $username %>"> -->
+    <INPUT TYPE="text" NAME="username" VALUE="hslink">
+  </TD>
+</TR>
+<TR>
+  <TH ALIGN="right">Domain </TH>
+  <TD>
+    <!-- <INPUT TYPE="text" NAME="domain" VALUE="<%= $domain %>"> -->
+    <INPUT TYPE="text" NAME="domain" VALUE="pobox.com">
+  </TD>
+</TR>
+<!--<INPUT TYPE="hidden" NAME="domain" VALUE="myisp.com">-->
+<TR>
+  <TH ALIGN="right">Password </TH>
+  <TD>
+    <!-- <INPUT TYPE="password" NAME="password"> -->
+    <INPUT TYPE="password" NAME="password" VALUE="UwjM5zdb">
+  </TD>
+</TR>
+</TABLE>
+<BR><BR><INPUT TYPE="submit" VALUE="Login">
+</FORM></BODY></HTML>
+
diff --git a/fs_selfservice/FS-SelfService/cgi/make_payment.html b/fs_selfservice/FS-SelfService/cgi/make_payment.html
new file mode 100644 (file)
index 0000000..a1cda6d
--- /dev/null
@@ -0,0 +1,120 @@
+<HTML><HEAD><TITLE>MyAccount</TITLE></HEAD>
+<BODY BGCOLOR="#eeeeee"><FONT SIZE=5>MyAccount</FONT><BR><BR>
+<%= $url = "$selfurl?session=$session_id;action="; ''; %>
+<TABLE BORDER=0 CELLPADDING=4><TR><TD VALIGN="top" HEIGHT=384 BGCOLOR="#dddddd">
+<A HREF="<%= $url %>myaccount">MyAccount</A><BR>
+<!-- <A HREF="<%= $url %>other">SomethingElse</A><BR> -->
+</TD><TD VALIGN="top">
+<FONT SIZE=4>Make a payment</FONT><BR><BR>
+<FORM NAME="OneTrueForm" METHOD="POST" ACTION="<%=$selfurl%>" onSubmit="document.OneTrueForm.process.disabled=true">
+<INPUT TYPE="hidden" NAME="session" VALUE="<%=$session_id%>">
+<INPUT TYPE="hidden" NAME="action" VALUE="payment_results">
+<TABLE BGCOLOR="#cccccc">
+<TR>
+  <TD ALIGN="right">Amount&nbsp;Due</TD>
+  <TD>
+    <TABLE><TR><TD BGCOLOR="#ffffff">
+      $<%=sprintf("%.2f",$balance)%>
+    </TD></TR></TABLE>
+  </TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Payment&nbsp;amount</TD>
+  <TD>
+    <TABLE><TR><TD BGCOLOR="#ffffff">
+      $<INPUT TYPE="text" NAME="amount" SIZE=8 VALUE="<%=sprintf("%.2f",$balance)%>">
+    </TD></TR></TABLE>
+  </TD>
+</TR><TR>
+  <TD ALIGN="right">Card&nbsp;type</TD>
+  <TD>
+    <SELECT NAME="card_type"><OPTION></OPTION>
+      <%= foreach ( keys %card_types ) {
+            $selected = $card_type eq $card_types{$_} ? ' SELECTED' : '';
+            $OUT .= qq(<OPTION$selected VALUE="). $card_types{$_}. qq(">$_\n);
+      } %>
+    </SELECT>
+  </TD>
+</TD><TR>
+  <TD ALIGN="right">Card&nbsp;number</TD>
+  <TD>
+    <TABLE>
+      <TR>
+        <TD>
+          <INPUT TYPE="text" NAME="payinfo" SIZE=20 MAXLENGTH=19 VALUE="<%=$payinfo%>"> </TD>
+        <TD>Exp.</TD>
+        <TD>
+          <SELECT NAME="month">
+            <%= for ( ( map "0$_", 1 .. 9 ), 11, 12 ) {
+                  $OUT .= '<OPTION'. ($_ eq $month ? ' SELECTED' : ''). ">$_\n";
+            } %>
+          </SELECT>
+        </TD>
+        <TD> / </TD>
+        <TD>
+          <SELECT NAME="year">
+            <%= for ( 2003 .. 2012 ) {
+                  $OUT .= '<OPTION'. ($_ eq $year ? ' SELECTED' : ''). ">$_\n";
+            } %>
+          </SELECT>
+        </TD>
+      </TR>
+    </TABLE>
+  </TD>
+</TR><TR>
+  <TD ALIGN="right">Exact&nbsp;name&nbsp;on&nbsp;card</TD>
+  <TD><INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<%=$payname%>"></TD>
+</TR><TR>
+  <TD ALIGN="right">Card&nbsp;billing&nbsp;address</TD>
+  <TD>
+    <INPUT TYPE="text" SIZE=40 MAXLENGTH=80 NAME="address1" VALUE="<%=$address1%>">
+  </TD>
+</TR><TR>
+  <TD ALIGN="right">Address&nbsp;line&nbsp;2</TD>
+  <TD>
+    <INPUT TYPE="text" SIZE=40 MAXLENGTH=80 NAME="address2" VALUE="<%=$address2%>">
+  </TD>
+</TR><TR>
+  <TD ALIGN="right">City</TD>
+  <TD>
+    <TABLE>
+      <TR>
+        <TD>
+          <INPUT TYPE="text" NAME="city" SIZE="12" MAXLENGTH=80 VALUE="<%=$city%>">
+        </TD>
+        <TD>State</TD>
+        <TD>
+          <SELECT NAME="state">
+            <%= for ( @states ) {
+              $OUT .= '<OPTION'. ($_ eq $state ? ' SELECTED' : '' ). ">$_\n";
+            } %>
+          </SELECT>
+        </TD>
+        <TD>Zip</TD>
+        <TD>
+          <INPUT TYPE="text" NAME="zip" SIZE=11 MAXLENGTH=10 VALUE="<%=$zip%>">
+        </TD>
+      </TR>
+    </TABLE>
+  </TD>
+</TR><TR>
+  <TD COLSPAN=2>
+    <INPUT TYPE="checkbox" CHECKED NAME="save" VALUE="1">
+    Remember this information
+  </TD>
+</TR><TR>
+  <TD COLSPAN=2>
+    <INPUT TYPE="checkbox"<%= $payby eq 'CARD' ? ' CHECKED' : '' %> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }">
+    Charge future payments to this card automatically
+  </TD>
+</TR>
+</TABLE>
+<BR>
+<INPUT TYPE="hidden" NAME="paybatch" VALUE="<%=$paybatch%>">
+<INPUT TYPE="submit" NAME="process" VALUE="Process payment"> <!-- onClick="this.disabled=true"> -->
+</FORM>
+</TD></TR></TABLE>
+<HR>
+<FONT SIZE="-2">powered by <a href="http://www.sisd.com/freeside">freeside</a></FONT>
+</BODY></HTML>
+
diff --git a/fs_selfservice/FS-SelfService/cgi/myaccount.html b/fs_selfservice/FS-SelfService/cgi/myaccount.html
new file mode 100644 (file)
index 0000000..f48fded
--- /dev/null
@@ -0,0 +1,47 @@
+<HTML><HEAD><TITLE>MyAccount</TITLE></HEAD>
+<BODY BGCOLOR="#eeeeee"><FONT SIZE=5>MyAccount</FONT><BR><BR>
+<%= $url = "$selfurl?session=$session_id;action="; ''; %>
+<TABLE BORDER=0 CELLPADDING=4><TR><TD VALIGN="top" HEIGHT=384 BGCOLOR="#dddddd">
+<A HREF="<%= $url %>myaccount">MyAccount</A><BR>
+<!-- <A HREF="<%= $url %>other">SomethingElse</A><BR> -->
+</TD><TD VALIGN="top">
+
+Hello <%= $name %>!<BR><BR>
+<%= $small_custview %>
+<BR>
+<%= if ( $balance > 0 ) {
+  $OUT .= qq! <B><A HREF="${url}make_payment">Make a payment</A></B><BR><BR>!;
+} %>
+<%=
+  if ( @open_invoices ) {
+    $OUT .= '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=2 BGCOLOR="#eeeeee">'.
+            '<TR><TH BGCOLOR="#ff3333" COLSPAN=5>Open Invoices</TH><TD>';
+    my $link = qq!<A HREF="<%= $url %>myaccount!;
+    my $col1 = "ffffff";
+    my $col2 = "dddddd";
+    my $col = $col1;
+
+    foreach my $invoice ( @open_invoices ) {
+      my $td = qq!<TD BGCOLOR="#$col">!;
+      my $a=qq!<A HREF="${url}view_invoice;invnum=!. $invoice->{'invnum'}. '">';
+      $OUT .=
+        "<TR>$td${a}Invoice #". $invoice->{'invnum'}. "</A></TD>$td</TD>".
+        "$td$a". $invoice->{'date'}. "</A></TD>$td</TD>".
+        qq!<TD BGCOLOR="#$col" ALIGN="right">$a\$!. $invoice->{'owed'}.
+          '</A></TD>'.
+        '</TR>';
+      $col = $col eq $col1 ? $col2 : $col1;
+    }
+    $OUT .= '</TABLE>';
+  } else {
+    $OUT .= 'You have no outstanding invoices.<BR><BR>';
+  }
+%>
+
+</TD></TR></TABLE>
+<HR>
+<FONT SIZE="-2">powered by <a href="http://www.sisd.com/freeside">freeside</a></FONT>
+</BODY></HTML>
+
+
+
diff --git a/fs_selfservice/FS-SelfService/cgi/passwd.html b/fs_selfservice/FS-SelfService/cgi/passwd.html
new file mode 100644 (file)
index 0000000..fadc4df
--- /dev/null
@@ -0,0 +1,25 @@
+<html>
+  <head>
+    <title>Change password</title>
+  </head>
+  <body bgcolor="#e8e8e8">
+    <h3>Change password</h3>
+    <form action="/cgi-bin/fs_passwd.cgi" method="post">
+    <table bgcolor="#cccccc" border=0 cellspacing=2>
+      <tr><th align="right">Username</th>
+        <td><input type="text" name="username" size="18"></td>
+      </tr>
+      <tr><th align="right">Current password</th>
+        <td><input type="password" name="old_password" size="18"></td>
+      </tr>
+      <tr><th align="right">New password</th>
+        <td><input type="password" name="new_password" size="18"></td>
+      </tr>
+      <tr><th align="right">Re-enter new password</th>
+        <td><input type="password" name="new_password2" size="18"></td>
+      </tr>
+    </table>
+    <br><input type="submit" value="Change password">
+  </body>
+</html>
+
diff --git a/fs_selfservice/FS-SelfService/cgi/payment_results.html b/fs_selfservice/FS-SelfService/cgi/payment_results.html
new file mode 100644 (file)
index 0000000..92c8cf5
--- /dev/null
@@ -0,0 +1,18 @@
+<HTML><HEAD><TITLE>MyAccount</TITLE></HEAD>
+<BODY BGCOLOR="#eeeeee"><FONT SIZE=5>MyAccount</FONT><BR><BR>
+<%= $url = "$selfurl?session=$session_id;action="; ''; %>
+<TABLE BORDER=0 CELLPADDING=4><TR><TD VALIGN="top" HEIGHT=384 BGCOLOR="#dddddd">
+<A HREF="<%= $url %>myaccount">MyAccount</A><BR>
+<!-- <A HREF="<%= $url %>other">SomethingElse</A><BR> -->
+</TD><TD VALIGN="top">
+<FONT SIZE=4>Payment results</FONT><BR><BR>
+<%= if ( $error ) {
+  $OUT .= qq!<FONT SIZE="+1" COLOR="#ff0000">Error processing your payment: $error</FONT>!;
+} else {
+  $OUT .= 'Your payment was processed sucessfully.  Thank you.';
+} %>
+</TD></TR></TABLE>
+<HR>
+<FONT SIZE="-2">powered by <a href="http://www.sisd.com/freeside">freeside</a></FONT>
+</BODY></HTML>
+
diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi
new file mode 100644 (file)
index 0000000..6d6716d
--- /dev/null
@@ -0,0 +1,188 @@
+#!/usr/bin/perl -Tw
+
+use strict;
+use vars qw($cgi $session_id $form_max $template_dir);
+use subs qw(do_template);
+use CGI;
+use CGI::Carp qw(fatalsToBrowser);
+use Text::Template;
+use FS::SelfService qw( login customer_info invoice payment_info
+                        process_payment );
+
+$template_dir = '.';
+
+$form_max = 255;
+
+$cgi = new CGI;
+
+unless ( defined $cgi->param('session') ) {
+  do_template('login',{});
+  exit;
+}
+
+if ( $cgi->param('session') eq 'login' ) {
+
+  $cgi->param('username') =~ /^\s*([a-z0-9_\-\.\&]{0,$form_max})\s*$/i
+    or die "illegal username";
+  my $username = $1;
+
+  $cgi->param('domain') =~ /^\s*([\w\-\.]{0,$form_max})\s*$/
+    or die "illegal domain";
+  my $domain = $1;
+
+  $cgi->param('password') =~ /^(.{0,$form_max})$/
+    or die "illegal password";
+  my $password = $1;
+
+  my $rv = login(
+    'username' => $username,
+    'domain'   => $domain,
+    'password' => $password,
+  );
+  if ( $rv->{error} ) {
+    do_template('login', {
+      'error'    => $rv->{error},
+      'username' => $username,
+      'domain'   => $domain,
+    } );
+    exit;
+  } else {
+    $cgi->param('session' => $rv->{session_id} );
+    $cgi->param('action'  => 'myaccount' );
+  }
+}
+
+$session_id = $cgi->param('session');
+
+$cgi->param('action') =~
+    /^(myaccount|view_invoice|make_payment|payment_results)$/
+  or die "unknown action ". $cgi->param('action');
+my $action = $1;
+
+my $result = eval "&$action();";
+die $@ if $@;
+
+if ( $result->{error} eq "Can't resume session" ) { #ick
+  do_template('login',{});
+  exit;
+}
+
+#warn $result->{'open_invoices'};
+#warn scalar(@{$result->{'open_invoices'}});
+
+warn "processing template $action\n";
+do_template($action, {
+  'session_id' => $session_id,
+  %{$result}
+});
+
+#--
+
+sub myaccount { customer_info( 'session_id' => $session_id ); }
+
+sub view_invoice {
+
+  $cgi->param('invnum') =~ /^(\d+)$/ or die "illegal invnum";
+  my $invnum = $1;
+
+  invoice( 'session_id' => $session_id,
+           'invnum'     => $invnum,
+         );
+
+}
+
+sub make_payment {
+  payment_info( 'session_id' => $session_id );
+}
+
+sub payment_results {
+
+  use Business::CreditCard;
+
+  $cgi->param('amount') =~ /^\s*(\d+(\.\d{2})?)\s*$/
+    or die "illegal amount"; #!!!
+  my $amount = $1;
+
+  my $payinfo = $cgi->param('payinfo');
+  $payinfo =~ s/\D//g;
+  $payinfo =~ /^(\d{13,16})$/
+    #or $error ||= $init_data->{msgcat}{invalid_card}; #. $self->payinfo;
+    or die "illegal card"; #!!!
+  $payinfo = $1;
+  validate($payinfo)
+    #or $error ||= $init_data->{msgcat}{invalid_card}; #. $self->payinfo;
+    or die "invalid card"; #!!!
+  cardtype($payinfo) eq $cgi->param('card_type')
+    #or $error ||= $init_data->{msgcat}{not_a}. $cgi->param('CARD_type');
+    or die "not a ". $cgi->param('card_type');
+
+  $cgi->param('month') =~ /^(\d{2})$/ or die "illegal month";
+  my $month = $1;
+  $cgi->param('year') =~ /^(\d{4})$/ or die "illegal year";
+  my $year = $1;
+
+  $cgi->param('payname') =~ /^(.{0,80})$/ or die "illegal payname";
+  my $payname = $1;
+
+  $cgi->param('address1') =~ /^(.{0,80})$/ or die "illegal address1";
+  my $address1 = $1;
+
+  $cgi->param('address2') =~ /^(.{0,80})$/ or die "illegal address2";
+  my $address2 = $1;
+
+  $cgi->param('city') =~ /^(.{0,80})$/ or die "illegal city";
+  my $city = $1;
+
+  $cgi->param('state') =~ /^(.{2})$/ or die "illegal state";
+  my $state = $1;
+
+  $cgi->param('zip') =~ /^(.{0,10})$/ or die "illegal zip";
+  my $zip = $1;
+
+  my $save = 0;
+  $save = 1 if $cgi->param('save');
+
+  my $auto = 0;
+  $auto = 1 if $cgi->param('auto');
+
+  $cgi->param('paybatch') =~ /^([\w\-\.]+)$/ or die "illegal paybatch";
+  my $paybatch = $1;
+
+  process_payment(
+    'session_id' => $session_id,
+    'amount'     => $amount,
+    'payinfo'    => $payinfo,
+    'month'      => $month,
+    'year'       => $year,
+    'payname'    => $payname,
+    'address1'   => $address1,
+    'address2'   => $address2,
+    'city'       => $city,
+    'state'      => $state,
+    'zip'        => $zip,
+    'save'       => $save,
+    'auto'       => $auto,
+    'paybatch'   => $paybatch,
+  );
+
+}
+
+#--
+
+sub do_template {
+  my $name = shift;
+  my $fill_in = shift;
+
+  $cgi->delete_all();
+  $fill_in->{'selfurl'} = $cgi->self_url;
+
+  my $template = new Text::Template( TYPE    => 'FILE',
+                                     SOURCE  => "$template_dir/$name.html",
+                                     DELIMITERS => [ '<%=', '%>' ],
+                                     UNTAINT => 1,                    )
+    or die $Text::Template::ERROR;
+
+  print $cgi->header( '-expires' => 'now' ),
+        $template->fill_in( HASH => $fill_in );
+}
+
diff --git a/fs_selfservice/FS-SelfService/cgi/view_invoice.html b/fs_selfservice/FS-SelfService/cgi/view_invoice.html
new file mode 100644 (file)
index 0000000..d2b012b
--- /dev/null
@@ -0,0 +1,21 @@
+<HTML><HEAD><TITLE>MyAccount</TITLE></HEAD>
+<BODY BGCOLOR="#eeeeee"><FONT SIZE=5>MyAccount</FONT><BR><BR>
+<%= $url = "$selfurl?session=$session_id;action="; ''; %>
+<TABLE BORDER=0 CELLPADDING=4><TR><TD VALIGN="top" HEIGHT=384 BGCOLOR="#dddddd">
+<A HREF="<%= $url %>myaccount">MyAccount</A><BR>
+<!-- <A HREF="<%= $url %>other">SomethingElse</A><BR> -->
+</TD><TD VALIGN="top">
+
+<A HREF="<%= $url %>myaccount"><-- back to MyAccount</A><BR><BR>
+
+<FONT SIZE="-1"><PRE>
+<%= $invoice_text %>
+</FONT></PRE>
+
+</TD></TR></TABLE>
+<HR>
+<FONT SIZE="-2">powered by <a href="http://www.sisd.com/freeside">freeside</a></FONT>
+</BODY></HTML>
+
+
+
diff --git a/fs_selfservice/FS-SelfService/freeside-selfservice-clientd b/fs_selfservice/FS-SelfService/freeside-selfservice-clientd
new file mode 100644 (file)
index 0000000..f13dd42
--- /dev/null
@@ -0,0 +1,226 @@
+#!/usr/bin/perl -w
+#
+# freeside-selfservice-clientd
+#
+# This is run REMOTELY over ssh by freeside-selfservice-server
+
+use strict;
+use subs qw(spawn logmsg);
+use Fcntl qw(:flock);
+use POSIX qw(:sys_wait_h);
+use Socket;
+use Storable qw(nstore_fd fd_retrieve);
+use IO::Handle qw(_IONBF);
+use IO::Select;
+use IO::File;
+
+#STDOUT->setbuf('');
+
+use vars qw( $Debug );
+$Debug = 3; #2 will turn on child logging, 3 will log packet contents,
+            #including potentially compromising information
+
+my $socket = "/usr/local/freeside/selfservice_socket";
+my $pid_file = "$socket.pid";
+
+my $log_file = "/usr/local/freeside/selfservice.log";
+
+#my $me = '[client]';
+
+$|=1;
+
+$SIG{__WARN__} = \&_logmsg;
+
+#read data to be cached or something
+#warn "$me Reading init data\n" if $Debug;
+#my $signup_init = 
+
+warn "Creating $socket\n" if $Debug;
+my $uaddr = sockaddr_un($socket);
+my $proto = getprotobyname('tcp');
+socket(Server,PF_UNIX,SOCK_STREAM,0) or die "socket: $!";
+unlink($socket);
+bind(Server, $uaddr) or die "bind: $!";
+listen(Server,SOMAXCONN) or die "listen: $!";
+
+if ( -e $pid_file ) {
+  open(PIDFILE,"<$pid_file");
+  my $old_pid = <PIDFILE>;
+  close PIDFILE;
+  $old_pid =~ /^(\d+)$/;
+  kill 'TERM', $1;
+}
+open(PIDFILE,">$pid_file");
+print PIDFILE "$$\n";
+close PIDFILE;
+
+#my $waitedpid;
+#sub REAPER { $waitedpid = wait; $SIG{CHLD} = \&REAPER; }
+#$SIG{CHLD} =  \&REAPER;
+
+warn "entering main loop\n" if $Debug;
+
+my %kids;
+
+my $s = new IO::Select;
+$s->add(\*STDIN);
+$s->add(\*Server);
+
+#for ( $waitedpid = 0;
+#      accept(Client,Server) || $waitedpid;
+#      $waitedpid = 0, close Client)
+#{
+#  next if $waitedpid;
+
+#$SIG{PIPE} = sub { warn "SIGPIPE received" };
+#$SIG{CHLD} = sub { warn "SIGCHLD received" };
+
+#sub REAPER { warn "SIGCHLD received"; my $pid = wait; $SIG{CHLD} = \&REAPER; }
+#sub REAPER { my $pid = wait; $SIG{CHLD} = \&REAPER; }
+#sub REAPER { my $pid = wait; delete $kids{$pid}; $SIG{CHLD} = \&REAPER; }
+#$SIG{CHLD} =  \&REAPER;
+
+my $undisp = 0;
+while (1) {
+
+  &reap_kids;
+
+  warn "waiting for connection\n" if $Debug && !$undisp;
+
+  #my @handles = $s->can_read();
+  my @handles = $s->can_read(5);
+  $undisp = !scalar(@handles);
+  foreach my $handle ( @handles ) {
+
+    if ( $handle == \*STDIN ) {
+
+      warn "receiving packet from server\n" if $Debug;
+
+      my $packet = fd_retrieve(\*STDIN);
+      my $token = $packet->{'_token'};
+      warn "received packet from server with token $token\n".
+           ( $Debug > 2
+             ? join('', map { " $_=>$packet->{$_}\n" } keys %$packet )
+             : '' )
+        if $Debug;
+
+     if ( exists($kids{$token}) ) {
+        warn "sending return packet to $token via $kids{$token}\n"
+          if $Debug;
+        nstore_fd($packet, $kids{$token});
+        warn "flushing to $token\n" if $Debug;
+        until ( $kids{$token}->flush ) {
+          warn "WARNING: error flushing: $!";
+          sleep 1;
+        }
+        #no close or delete here - will block waiting for child
+        warn "done with $token\n" if $Debug;
+      } else {
+        warn "WARNING: unknown token $token, discarding message";
+      }
+
+    } elsif ( $handle == \*Server ) {
+
+      until ( accept(Client, Server) ) {
+        warn "WARNING: accept failed: $!";
+        next;
+      }
+
+      warn "received local connection; forking\n" if $Debug;
+
+      spawn sub { #child
+        warn "[child-$$] reading packet from local client" if $Debug > 1;
+        my $packet = fd_retrieve(\*Client);
+        warn "[child-$$] packet received:\n".
+             join('', map { " $_=>$packet->{$_}\n" } keys %$packet )
+          if $Debug > 2;
+        my $command = $packet->{'command'};
+        #handle some commands weirdly?
+        $packet->{_token}=$$;
+
+        warn "[child-$$] sending packet to remote server" if $Debug > 1;
+        flock(STDOUT, LOCK_EX) or die "FATAL: can't lock write stream: $!";
+        nstore_fd($packet, \*STDOUT) or die "FATAL: can't send response: $!";
+        STDOUT->flush or die "FATAL: can't flush: $!";
+        flock(STDOUT, LOCK_UN) or die "FATAL: can't release write lock: $!";
+        close STDOUT or die "FATAL: can't close write stream: $!"; #??!
+
+        warn "[child-$$] waiting for response from parent" if $Debug > 1;
+        my $w = new IO::Select;
+        $w->add(\*STDIN);
+        until ( $w->can_read ) {
+          warn "[child-$$] WARNING: interrupted select: $!\n";
+        }
+        my $rv = fd_retrieve(\*STDIN);
+
+        #close STDIN;
+
+        warn "[child-$$] sending response to local client" if $Debug > 1;
+        nstore_fd($rv, \*Client);
+        Client->flush or die "FATAL: can't flush to local client: $!";
+        close Client or die "FATAL: can't close connection to local client: $!";
+
+        warn "[child-$$] child exiting" if $Debug > 1;
+        exit;
+
+      }; #eo child
+
+      #close Client;
+
+    } else {
+      die "wtf?  $handle";
+    }
+
+  }
+  
+}
+
+sub reap_kids {
+  #warn "reaping kids\n";
+  foreach my $pid ( keys %kids ) {
+    my $kid = waitpid($pid, WNOHANG);
+    if ( $kid > 0 ) {
+      close $kids{$kid};
+      delete $kids{$kid};
+    }
+  }
+  #warn "done reaping\n";
+}
+
+sub spawn {
+    my $coderef = shift;
+
+    unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') {
+        use Carp;
+        confess "usage: spawn CODEREF";
+    }
+
+    my $pid;
+    #if (!defined($pid = fork)) {
+    my $kid = new IO::Handle;
+    if (!defined($pid = open($kid, '|-'))) {
+        warn "WARNING: cannot fork: $!";
+        return;
+    } elsif ($pid) {
+        warn "begat $pid" if $Debug;
+        $kids{$pid} = $kid;
+        #$kids{$pid}->autoflush;
+        return; # I'm the parent
+    }
+    # else I'm the child -- go spawn
+
+#    open(STDIN,  "<&Client")   || die "can't dup client to stdin";
+#    open(STDOUT, ">&Client")   || die "can't dup client to stdout";
+#     open(STDERR, ">&STDOUT") || die "can't dup stdout to stderr";
+    exit &$coderef();
+}
+
+sub _logmsg {
+  chomp( my $msg = shift );
+  my $log = new IO::File ">>$log_file";
+  flock($log, LOCK_EX);
+  seek($log, 0, 2);
+  print $log "[client] [". scalar(localtime). "] [$$] $msg\n";
+  flock($log, LOCK_UN);
+  close $log;
+}
diff --git a/fs_selfservice/FS-SelfService/test.pl b/fs_selfservice/FS-SelfService/test.pl
new file mode 100644 (file)
index 0000000..7468ea4
--- /dev/null
@@ -0,0 +1,17 @@
+# Before `make install' is performed this script should be runnable with
+# `make test'. After `make install' it should work as `perl test.pl'
+
+#########################
+
+# change 'tests => 1' to 'tests => last_test_to_print';
+
+use Test;
+BEGIN { plan tests => 1 };
+use FS::SelfService;
+ok(1); # If we made it this far, we're ok.
+
+#########################
+
+# Insert your test code below, the Test module is use()ed here so read
+# its man page ( perldoc Test ) for help writing this test script.
+
diff --git a/fs_selfservice/fs_passwd_test b/fs_selfservice/fs_passwd_test
new file mode 100755 (executable)
index 0000000..4f8b8a8
--- /dev/null
@@ -0,0 +1,19 @@
+#!/usr/bin/perl -w
+
+use strict;
+use FS::SelfService qw(passwd);
+
+my $rv = passwd(
+  'username' => 'ivan',
+  'old_password' => 'heyhoo',
+  'new_password' => 'haloo',
+);
+my $error = $rv->{error};
+
+if ( $error eq 'Incorrect password.' ) {
+  exit;
+} else {
+  die $error if $error;
+  die "no error";
+}
+
diff --git a/fs_sesmon/FS-SessionClient/Changes b/fs_sesmon/FS-SessionClient/Changes
new file mode 100644 (file)
index 0000000..390a7b9
--- /dev/null
@@ -0,0 +1,5 @@
+Revision history for Perl extension FS::SessionClient
+
+0.01  Wed Oct 18 16:34:36 1999
+        - original version; created by ivan 1.0
+
diff --git a/fs_sesmon/FS-SessionClient/MANIFEST b/fs_sesmon/FS-SessionClient/MANIFEST
new file mode 100644 (file)
index 0000000..162d4e4
--- /dev/null
@@ -0,0 +1,11 @@
+Changes
+MANIFEST
+MANIFEST.SKIP
+Makefile.PL
+SessionClient.pm
+test.pl
+fs_sessiond
+cgi/login.cgi
+cgi/logout.cgi
+bin/freeside-login
+bin/freeside-logout
diff --git a/fs_sesmon/FS-SessionClient/MANIFEST.SKIP b/fs_sesmon/FS-SessionClient/MANIFEST.SKIP
new file mode 100644 (file)
index 0000000..ae335e7
--- /dev/null
@@ -0,0 +1 @@
+CVS/
diff --git a/fs_sesmon/FS-SessionClient/Makefile.PL b/fs_sesmon/FS-SessionClient/Makefile.PL
new file mode 100644 (file)
index 0000000..137b6b8
--- /dev/null
@@ -0,0 +1,10 @@
+use ExtUtils::MakeMaker;
+# See lib/ExtUtils/MakeMaker.pm for details of how to influence
+# the contents of the Makefile that is written.
+WriteMakefile(
+    'NAME'          => 'FS::SessionClient',
+    'VERSION_FROM'  => 'SessionClient.pm', # finds $VERSION
+    'EXE_FILES'     => [ qw(fs_sessiond bin/freeside-login bin/freeside-logout) ],
+    'INSTALLSCRIPT' => '/usr/local/sbin',
+    'PERM_RWX'      => '750',
+);
diff --git a/fs_sesmon/FS-SessionClient/SessionClient.pm b/fs_sesmon/FS-SessionClient/SessionClient.pm
new file mode 100644 (file)
index 0000000..8a0ff70
--- /dev/null
@@ -0,0 +1,122 @@
+package FS::SessionClient;
+
+use strict;
+use vars qw($AUTOLOAD $VERSION @ISA @EXPORT_OK $fs_sessiond_socket);
+use Exporter;
+use Socket;
+use FileHandle;
+use IO::Handle;
+
+$VERSION = '0.01';
+
+@ISA = qw( Exporter );
+@EXPORT_OK = qw( login logout portnum );
+
+$fs_sessiond_socket = "/usr/local/freeside/fs_sessiond_socket";
+
+$ENV{'PATH'} ='/usr/bin:/bin';
+$ENV{'SHELL'} = '/bin/sh';
+$ENV{'IFS'} = " \t\n";
+$ENV{'CDPATH'} = '';
+$ENV{'ENV'} = '';
+$ENV{'BASH_ENV'} = '';
+
+my $freeside_uid = scalar(getpwnam('freeside'));
+die "not running as the freeside user\n" if $> != $freeside_uid;
+
+=head1 NAME
+
+FS::SessionClient - Freeside session client API
+
+=head1 SYNOPSIS
+
+  use FS::SessionClient qw( login portnum logout );
+
+  $error = login ( {
+    'username' => $username,
+    'password' => $password,
+    'login'    => $timestamp,
+    'portnum'  => $portnum,
+  } );
+
+  $portnum = portnum( { 'ip' => $ip } ) or die "unknown ip!"
+  $portnum = portnum( { 'nasnum' => $nasnum, 'nasport' => $nasport } )
+    or die "unknown nasnum/nasport";
+
+  $error = logout ( {
+    'username' => $username,
+    'password' => $password,
+    'logout'   => $timestamp,
+    'portnum'  => $portnum,
+  } );
+
+=head1 DESCRIPTION
+
+This modules provides an API for a remote session application.
+
+It needs to be run as the freeside user.  Because of this, the program which
+calls these subroutines should be written very carefully.
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item login HASHREF
+
+HASHREF should have the following keys: username, password, login and portnum.
+login is a UNIX timestamp; if not specified, will default to the current time.
+Starts a new session for the specified user and portnum.  The password is
+optional, but must be correct if specified.
+
+Returns a scalar error message, or the empty string for success.
+
+=item portnum
+
+HASHREF should contain a single key: ip, or the two keys: nasnum and nasport.
+Returns a portnum suitable for the login and logout subroutines, or false
+on error.
+
+=item logout HASHREF
+
+HASHREF should have the following keys: usrename, password, logout and portnum.
+logout is a UNIX timestamp; if not specified, will default to the current time.
+Starts a new session for the specified user and portnum.  The password is
+optional, but must be correct if specified.
+
+Returns a scalar error message, or the empty string for success.
+
+=cut
+
+sub AUTOLOAD {
+  my $hashref = shift;
+  my $method = $AUTOLOAD;
+  $method =~ s/^.*:://;
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_sessiond_socket)) or die "connect: $!";
+  print SOCK "$method\n";
+
+  print SOCK join("\n", %{$hashref}, 'END' ), "\n";
+  SOCK->flush;
+
+  chomp( my $r = <SOCK> );
+  $r;
+}
+
+=back
+
+=head1 VERSION
+
+$Id: SessionClient.pm,v 1.3 2000-12-03 20:25:20 ivan Exp $
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<fs_sessiond>
+
+=cut
+
+1;
+
+
+
diff --git a/fs_sesmon/FS-SessionClient/bin/freeside-login b/fs_sesmon/FS-SessionClient/bin/freeside-login
new file mode 100644 (file)
index 0000000..a6d4751
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/perl -Tw
+
+#false-laziness hack w freeside-logout
+
+use strict;
+use FS::SessionClient qw( login portnum );
+
+my $username = shift;
+
+my $portnum;
+if ( scalar(@ARGV) == 1 ) {
+  my $arg = shift;
+  if ( $arg =~ /^(\d+)$/ ) {
+    $portnum = $1;
+  } elsif ( $arg =~ /^([\d\.]+)$/ ) {
+    $portnum = portnum( { 'ip' => $1 } ) or die "unknown ip!"
+  } else {
+    &usage;
+  }
+} elsif ( scalar(@ARGV) == 2 ) {
+  $portnum = portnum( { 'nasnum' => shift, 'nasport' => shift } )
+    or die "unknown nasnum/nasport";
+} else {
+  &usage;
+}
+
+my $error = login ( {
+  'username' => $username,
+  'portnum'  => $portnum,
+} );
+
+warn $error if $error;
+
+sub usage {
+  die "Usage:\n\n  freeside-login username ( portnum | ip | nasnum nasport )";
+}
diff --git a/fs_sesmon/FS-SessionClient/bin/freeside-logout b/fs_sesmon/FS-SessionClient/bin/freeside-logout
new file mode 100644 (file)
index 0000000..9b4ecfe
--- /dev/null
@@ -0,0 +1,36 @@
+#!/usr/bin/perl -Tw
+
+#false-laziness hack w freeside-login
+
+use strict;
+use FS::SessionClient qw( logout portnum );
+
+my $username = shift;
+
+my $portnum;
+if ( scalar(@ARGV) == 1 ) {
+  my $arg = shift;
+  if ( $arg =~ /^(\d+)$/ ) {
+    $portnum = $1;
+  } elsif ( $arg =~ /^([\d\.]+)$/ ) {
+    $portnum = portnum( { 'ip' => $1 } ) or die "unknown ip!"
+  } else {
+    &usage;
+  }
+} elsif ( scalar(@ARGV) == 2 ) {
+  $portnum = portnum( { 'nasnum' => shift, 'nasport' => shift } )
+    or die "unknown nasnum/nasport";
+} else {
+  &usage;
+}
+
+my $error = logout ( {
+  'username' => $username,
+  'portnum'  => $portnum,
+} );
+
+warn $error if $error;
+
+sub usage {
+  die "Usage:\n\n  freeside-logout username ( portnum | ip | nasnum nasport )";
+}
diff --git a/fs_sesmon/FS-SessionClient/cgi/login.cgi b/fs_sesmon/FS-SessionClient/cgi/login.cgi
new file mode 100644 (file)
index 0000000..0307c5a
--- /dev/null
@@ -0,0 +1,108 @@
+#!/usr/bin/perl -Tw
+
+#false-laziness hack w logout.cgi
+
+use strict;
+use vars qw( $cgi $username $password $error $ip $portnum );
+use CGI;
+use CGI::Carp qw(fatalsToBrowser);
+use FS::SessionClient qw( login portnum );
+
+$cgi = new CGI;
+
+if ( defined $cgi->param('magic') ) {
+  $cgi->param('username') =~ /^\s*(\w{1,255})\s*$/ or do {
+    $error = "Illegal username";
+    &print_form;
+    exit;
+  };
+  $username = $1;
+  $cgi->param('password') =~ /^([^\n]{0,255})$/ or die "guru meditation #420";
+  $password = $1;
+  #$ip = $cgi->remote_host;
+  $ip = $ENV{REMOTE_ADDR};
+  $ip =~ /^([\d\.]+)$/ or die "illegal ip: $ip";
+  $ip = $1;
+  $portnum = portnum( { 'ip' => $1 } ) or do {
+    $error = "You appear to be coming from an unknown IP address.  Verify ".
+             "that your computer is set to obtain an IP address automatically ".
+             "via DHCP.";
+    &print_form;
+    exit;
+  };
+
+  ( $error = login ( {
+    'username' => $username,
+    'portnum'  => $portnum,
+    'password' => $password,
+  } ) )
+    ? &print_form()
+    : &print_okay();
+
+} else {
+  $username = '';
+  $password = '';
+  $error = '';
+  &print_form;
+}
+
+sub print_form {
+  my $self_url = $cgi->self_url;
+
+  print $cgi->header( '-expires' => 'now' ), <<END;
+<HTML><HEAD><TITLE>login</TITLE></HEAD>
+<BODY BGCOLOR="#FFFFFF">
+END
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: $error</FONT>! if $error;
+
+print <<END;
+<FORM ACTION="$self_url" METHOD="POST">
+<INPUT TYPE="hidden" NAME="magic" VALUE="process">
+<TABLE BORDER="0" CELLSPACING="0" CELLPADDING="4" ALIGN="center">
+<TR>
+        <TD ALIGN="center" COLSPAN="2">
+      <STRONG>Welcome</STRONG>
+      </TD>
+</TR>
+<TR>
+      <TD ALIGN="right">
+      Username
+      </TD>
+      <TD ALIGN="left">
+      <INPUT TYPE="text" NAME="username" VALUE="$username">
+      </TD>
+</TR>
+<TR>
+      <TD ALIGN="right">
+      Password
+      </TD>
+      <TD ALIGN="left">
+      <INPUT TYPE="password" NAME="password">
+      </TD>
+</TR>
+<TR>
+      <TD ALIGN="center" COLSPAN="2">
+      <INPUT TYPE="submit" VALUE=" Login ">
+      </TD>
+</TR>
+</TABLE>
+</FORM>
+</BODY>
+</HTML>
+END
+
+}
+
+sub print_okay {
+  print $cgi->header( '-expires' => 'now' ), <<END;
+<HTML><HEAD><TITLE>login sucessful</TITLE></HEAD>
+<BODY>login successful, etc.
+</BODY>
+</HTML>
+END
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-login username ( portnum | ip | nasnum nasport )";
+}
diff --git a/fs_sesmon/FS-SessionClient/cgi/logout.cgi b/fs_sesmon/FS-SessionClient/cgi/logout.cgi
new file mode 100644 (file)
index 0000000..95cef98
--- /dev/null
@@ -0,0 +1,83 @@
+#!/usr/bin/perl -Tw
+
+#false-laziness hack w login.cgi
+
+use strict;
+use vars qw( $cgi $username $password $error $ip $portnum );
+use CGI;
+use CGI::Carp qw(fatalsToBrowser);
+use FS::SessionClient qw( logout portnum );
+
+$cgi = new CGI;
+
+if ( defined $cgi->param('magic') ) {
+  $cgi->param('username') =~ /^\s*(\w{1,255})\s*$/ or do {
+    $error = "Illegal username";
+    &print_form;
+    exit;
+  };
+  $username = $1;
+  $cgi->param('password') =~ /^([^\n]{0,255})$/ or die "guru meditation #420";
+  $password = $1;
+  #$ip = $cgi->remote_host;
+  $ip = $ENV{REMOTE_ADDR};
+  $ip =~ /^([\d\.]+)$/ or die "illegal ip: $ip";
+  $ip = $1;
+  $portnum = portnum( { 'ip' => $1 } ) or do {
+    $error = "You appear to be coming from an unknown IP address.  Verify ".
+             "that your computer is set to obtain an IP address automatically ".
+             "via DHCP.";
+    &print_form;
+    exit;
+  };
+
+  ( $error = logout ( {
+    'username' => $username,
+    'portnum'  => $portnum,
+    'password' => $password,
+  } ) )
+    ? &print_form()
+    : &print_okay();
+
+} else {
+  $username = '';
+  $password = '';
+  $error = '';
+  &print_form;
+}
+
+sub print_form {
+  my $self_url = $cgi->self_url;
+
+  print $cgi->header( '-expires' => 'now' ), <<END;
+<HTML><HEAD><TITLE>logout</TITLE></HEAD>
+<BODY>
+END
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: $error</FONT>! if $error;
+
+print <<END;
+<FORM ACTION="$self_url" METHOD=POST>
+<INPUT TYPE="hidden" NAME="magic" VALUE="process">
+Username <INPUT TYPE="text" NAME="username" VALUE="$username"><BR>
+Password <INPUT TYPE="password" NAME="password"><BR>
+<INPUT TYPE="submit">
+</FORM>
+</BODY>
+</HTML>
+END
+
+}
+
+sub print_okay {
+  print $cgi->header( '-expires' => 'now' ), <<END;
+<HTML><HEAD><TITLE>logout sucessful</TITLE></HEAD>
+<BODY>logout successful, etc.
+</BODY>
+</HTML>
+END
+}
+
+sub usage {
+  die "Usage:\n\n  freeside-logout username ( portnum | ip | nasnum nasport )";
+}
diff --git a/fs_sesmon/FS-SessionClient/fs_sessiond b/fs_sesmon/FS-SessionClient/fs_sessiond
new file mode 100644 (file)
index 0000000..bfdb20a
--- /dev/null
@@ -0,0 +1,65 @@
+#!/usr/bin/perl -Tw
+#
+# fs_sessiond
+#
+# This is run REMOTELY over ssh by fs_session_server
+#
+
+use strict;
+use Socket;
+
+use vars qw( $Debug );
+
+$Debug = 1;
+
+my $fs_sessiond_socket = "/usr/local/freeside/fs_sessiond_socket";
+
+$ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
+$ENV{'SHELL'} = '/bin/sh';
+$ENV{'IFS'} = " \t\n";
+$ENV{'CDPATH'} = '';
+$ENV{'ENV'} = '';
+$ENV{'BASH_ENV'} = '';
+
+$|=1;
+
+my $me = "[fs_sessiond]";
+
+warn "$me starting\n" if $Debug;
+#nothing to read from server
+
+warn "$me creating $fs_sessiond_socket\n" if $Debug;
+my $uaddr = sockaddr_un($fs_sessiond_socket);
+my $proto = getprotobyname('tcp');
+socket(Server,PF_UNIX,SOCK_STREAM,0) or die "socket: $!";
+unlink($fs_sessiond_socket);
+bind(Server, $uaddr) or die "bind: $!";
+listen(Server,SOMAXCONN) or die "listen: $!";
+
+warn "$me entering main loop\n" if $Debug;
+my $paddr;
+for ( ; $paddr = accept(Client,Server); close Client) {
+
+  chomp( my $command = <Client> );
+
+  if ( $command eq 'login' || $command eq 'logout' || $command eq 'portnum' ) {
+    warn "$me reading data from local client\n" if $Debug;
+    my @data;
+    my $dos = 0;
+    push @data, scalar(<Client>) until $dos++ == 99 || $data[$#data] eq "END\n";
+    if ( $dos == 99 ) { 
+      warn "$me WARNING: DoS attempt!" 
+    } else {
+      warn "$me sending data to remote server\n" if $Debug;
+      print "$command\n", @data;
+      warn "$me reading result from remote server\n" if $Debug;
+      my $error = <STDIN>;
+      warn "$me sending error to local client\n" if $Debug;
+      print Client $error;
+    }
+  } else {
+    warn "$me WARNING: unexpected command from client: $command";
+  }
+
+}
+
diff --git a/fs_sesmon/FS-SessionClient/test.pl b/fs_sesmon/FS-SessionClient/test.pl
new file mode 100644 (file)
index 0000000..4b9ae17
--- /dev/null
@@ -0,0 +1,21 @@
+# Before `make install' is performed this script should be runnable with
+# `make test'. After `make install' it should work as `perl test.pl'
+
+######################### We start with some black magic to print on failure.
+
+# Change 1..1 below to 1..last_test_to_print .
+# (It may become useful if the test is moved to ./t subdirectory.)
+
+BEGIN { $| = 1; print "1..1\n"; }
+END {print "not ok 1\n" unless $loaded;}
+#use FS::SessionClient;
+#sigh, "not running as the freeside user"
+$loaded = 1;
+print "ok 1\n";
+
+######################### End of black magic.
+
+# Insert your test code below (better if it prints "ok 13"
+# (correspondingly "not ok 13") depending on the success of chunk 13
+# of the test code):
+
diff --git a/fs_sesmon/fs_session_server b/fs_sesmon/fs_session_server
new file mode 100644 (file)
index 0000000..00229f8
--- /dev/null
@@ -0,0 +1,140 @@
+#!/usr/bin/perl -Tw
+#
+# fs_session_server
+#
+
+use strict;
+use vars qw( $opt $Debug );
+use IO::Handle;
+use Net::SSH qw(sshopen2);
+use FS::UID qw(adminsuidsetup dbh);
+use FS::Record qw( qsearchs ); #qsearch );
+#use FS::cust_main_county;
+#use FS::cust_main;
+use FS::session;
+use FS::port;
+use FS::svc_acct;
+
+#require "configfile";
+$Debug = 1;
+
+my $user = shift or die &usage;
+&adminsuidsetup( $user ); 
+
+my $machine = shift or die &usage;
+
+my $fs_sessiond = "/usr/local/sbin/fs_sessiond";
+
+my $me = "[fs_session_server]";
+
+while (1) {
+  my($reader, $writer) = (new IO::Handle, new IO::Handle);
+  $writer->autoflush(1);
+  warn "$me Connecting to $machine\n" if $Debug;
+  sshopen2($machine,$reader,$writer,$fs_sessiond);
+
+  warn "$me Entering main loop\n" if $Debug;
+  while (1) {
+    warn "$me Reading (waiting for) data\n" if $Debug;
+    my $command = scalar(<$reader>);
+    chomp $command;
+    #DoS protection here too, to protect against a compromised client?  *sigh*
+    my %hash;
+    while ( ( my $key = scalar(<$reader>) ) ne "END\n" ) {
+      chomp $key;
+      chomp( $hash{$key} = scalar(<$reader>) );
+    }
+
+    if ( $command eq 'login' ) {
+      my $error = &login(\%hash);
+      print $writer "$error\n";
+    } elsif ( $command eq 'logout' ) {
+      my $error = &logout(\%hash);
+      print $writer "$error\n";
+    } elsif ( $command eq 'portnum' ) {
+      my $port;
+      if ( exists $hash{'ip'} ) {
+        $hash{'ip'} =~ /^([\d\.]+)$/ or $1='nomatch';
+        $port = qsearchs('port', { 'ip' => $1 } );
+      } else {
+        $hash{'nasnum'} =~ /^(\d+)$/ and my $nasnum = $1;
+        $hash{'nasport'} =~ /^(\d+)$/ and my $nasport = $1;
+        $port = qsearchs('port', { 'nasnum'=>$nasnum, 'nasport'=>$nasport } );
+      }
+      print $writer ( $port ? $port->portnum : '' ), "\n";
+    } else {
+      warn "$me WARNING: unrecognized command: $command";
+    }
+  }
+  #won't ever reach without code above to throw out of loop, but...
+  close $writer;
+  close $reader;
+  warn "connection to $machine lost!\n";
+  sleep 5;
+  warn "reconnecting...\n";
+}
+
+sub login {
+  my $href = shift;
+  $href->{'username'} =~ /^([a-z0-9_\-\.]+)$/ or return "Illegal username";
+  my $username = $1;
+  my $svc_acct = qsearchs('svc_acct', { 'username' => $username } )
+    or return "Unknown user";
+  return "Incorrect password"
+    if exists($href->{'password'})
+       && $href->{'password'} ne $svc_acct->_password;
+  return "Time limit exceeded" unless $svc_acct->seconds;
+  my $session = new FS::session {
+    'portnum' => $href->{'portnum'},
+    'svcnum'  => $svc_acct->svcnum,
+    'login'   => $href->{'login'},
+  };
+  $session->insert;
+}
+
+sub logout {
+  my $href = shift;
+  $href->{'username'} =~ /^([a-z0-9_\-\.]+)$/ or return "Illegal username";
+  my $username = $1;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+  my $svc_acct =
+    qsearchs('svc_acct', { 'username' => $username }, '', 'FOR UPDATE' )
+    or return "Unknown user";
+  return "Incorrect password"
+    if exists($href->{'password'})
+       && $href->{'password'} ne $svc_acct->_password;
+  my $session = qsearchs( 'session', {
+                                       'portnum' => $href->{'portnum'},
+                                       'svcnum'  => $svc_acct->svcnum,
+                                       'logout'  => '',
+                                     },
+                          '', 'FOR UPDATE'
+  );
+  unless ( $session ) {
+    $dbh->rollback;
+    return "No currently open sessions found for that user/port!";
+  }
+  my $nsession = new FS::session ( { $session->hash } );
+  warn "$nsession replacing $session";
+  my $error = $nsession->replace($session);
+  if ( $error ) {
+    $dbh->rollback;
+    return "can't logout: $error";
+  }
+  my $time = $nsession->logout - $nsession->login;
+  my $new_svc_acct = new FS::svc_acct ( { $svc_acct->hash } );
+  my $seconds = $new_svc_acct->seconds;
+  $seconds -= $time;
+  $seconds = 0 if $seconds < 0;
+  $new_svc_acct->seconds( $seconds );
+  $error = $new_svc_acct->replace( $svc_acct );
+  warn "can't debit time: $error\n"; #don't want to rollback, though
+  $dbh->commit or die $dbh->errstr;
+  ''
+}
+
+sub usage {
+  die "Usage:\n\n  fs_session_server user machine\n";
+}
+
diff --git a/fs_signup/FS-SignupClient/Changes b/fs_signup/FS-SignupClient/Changes
new file mode 100644 (file)
index 0000000..e750a82
--- /dev/null
@@ -0,0 +1,5 @@
+Revision history for Perl extension FS::SignupClient.
+
+0.01  Mon Aug 23 01:12:46 1999
+       - original version; created by h2xs 1.19
+
diff --git a/fs_signup/FS-SignupClient/MANIFEST b/fs_signup/FS-SignupClient/MANIFEST
new file mode 100644 (file)
index 0000000..b4a9900
--- /dev/null
@@ -0,0 +1,8 @@
+Changes
+MANIFEST
+MANIFEST.SKIP
+Makefile.PL
+SignupClient.pm
+test.pl
+fs_signupd
+cgi/signup.cgi
diff --git a/fs_signup/FS-SignupClient/MANIFEST.SKIP b/fs_signup/FS-SignupClient/MANIFEST.SKIP
new file mode 100644 (file)
index 0000000..ae335e7
--- /dev/null
@@ -0,0 +1 @@
+CVS/
diff --git a/fs_signup/FS-SignupClient/Makefile.PL b/fs_signup/FS-SignupClient/Makefile.PL
new file mode 100644 (file)
index 0000000..e740519
--- /dev/null
@@ -0,0 +1,18 @@
+use ExtUtils::MakeMaker;
+# See lib/ExtUtils/MakeMaker.pm for details of how to influence
+# the contents of the Makefile that is written.
+WriteMakefile(
+    'NAME'         => 'FS::SignupClient',
+    'VERSION_FROM'  => 'SignupClient.pm', # finds $VERSION
+    'EXE_FILES'     => [ 'fs_signupd' ],
+    'INSTALLSCRIPT' => '/usr/local/sbin',
+    'INSTALLSITEBIN' => '/usr/local/sbin',
+    'PERM_RWX'      => '750',
+    'PREREQ_PM'     => {
+                         'Business::CreditCard' => 0,
+                         'HTTP::BrowserDetect' => 0,
+                         'HTTP::Headers::UserAgent' => 3,
+                         'Storable' => 0,
+                         'Text::Template' => 0,
+                       },
+);
diff --git a/fs_signup/FS-SignupClient/SignupClient.pm b/fs_signup/FS-SignupClient/SignupClient.pm
new file mode 100644 (file)
index 0000000..842064d
--- /dev/null
@@ -0,0 +1,189 @@
+package FS::SignupClient;
+
+use strict;
+use vars qw($VERSION @ISA @EXPORT_OK $fs_signupd_socket);
+use Exporter;
+use Socket;
+use FileHandle;
+use IO::Handle;
+use Storable qw(nstore_fd fd_retrieve);
+
+$VERSION = '0.03';
+
+@ISA = qw( Exporter );
+@EXPORT_OK = qw( signup_info new_customer );
+
+$fs_signupd_socket = "/usr/local/freeside/fs_signupd_socket";
+
+$ENV{'PATH'} ='/usr/bin:/usr/ucb:/bin';
+$ENV{'SHELL'} = '/bin/sh';
+$ENV{'IFS'} = " \t\n";
+$ENV{'CDPATH'} = '';
+$ENV{'ENV'} = '';
+$ENV{'BASH_ENV'} = '';
+
+my $freeside_uid = scalar(getpwnam('freeside'));
+die "not running as the freeside user\n" if $> != $freeside_uid;
+
+=head1 NAME
+
+FS::SignupClient - Freeside signup client API
+
+=head1 SYNOPSIS
+
+  use FS::SignupClient qw( signup_info new_customer );
+
+  ( $locales, $packages, $pops ) = signup_info;
+
+  $error = new_customer ( {
+    'first'            => $first,
+    'last'             => $last,
+    'ss'               => $ss,
+    'comapny'          => $company,
+    'address1'         => $address1,
+    'address2'         => $address2,
+    'city'             => $city,
+    'county'           => $county,
+    'state'            => $state,
+    'zip'              => $zip,
+    'country'          => $country,
+    'daytime'          => $daytime,
+    'night'            => $night,
+    'fax'              => $fax,
+    'payby'            => $payby,
+    'payinfo'          => $payinfo,
+    'paydate'          => $paydate,
+    'payname'          => $payname,
+    'invoicing_list'   => $invoicing_list,
+    'referral_custnum' => $referral_custnum,
+    'comments'         => $comments,
+    'pkgpart'          => $pkgpart,
+    'username'         => $username,
+    '_password'        => $password,
+    'sec_phrase'       => $sec_phrase,
+    'popnum'           => $popnum,
+    'agentnum'         => $agentnum, #optional
+  } );
+
+=head1 DESCRIPTION
+
+This module provides an API for a remote signup server.
+
+It needs to be run as the freeside user.  Because of this, the program which
+calls these subroutines should be written very carefully.
+
+=head1 SUBROUTINES
+
+=over 4
+
+=item signup_info
+
+Returns three array references of hash references.
+
+The first set of hash references is of allowable locales.  Each hash reference
+has the following keys:
+  taxnum
+  state
+  county
+  country
+
+The second set of hash references is of allowable packages.  Each hash
+reference has the following keys:
+  pkgpart
+  pkg
+
+The third set of hash references is of allowable POPs (Points Of Presence).
+Each hash reference has the following keys:
+  popnum
+  city
+  state
+  ac
+  exch
+
+(Future expansion: fourth argument is the $init_data hash reference)
+
+=cut
+
+sub signup_info {
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_signupd_socket)) or die "connect: $!";
+  print SOCK "signup_info\n";
+  SOCK->flush;
+
+  my $init_data = fd_retrieve(\*SOCK);
+  close SOCK;
+
+  (map { $init_data->{$_} } qw( cust_main_county part_pkg svc_acct_pop ) ),
+  $init_data;
+
+}
+
+=item new_customer HASHREF
+
+Adds a customer to the remote Freeside system.  Requires a hash reference as
+a paramater with the following keys:
+  first
+  last
+  ss
+  comapny
+  address1
+  address2
+  city
+  county
+  state
+  zip
+  country
+  daytime
+  night
+  fax
+  payby
+  payinfo
+  paydate
+  payname
+  invoicing_list
+  referral_custnum
+  comments
+  pkgpart
+  username
+  _password
+  sec_phrase
+  popnum
+
+Returns a scalar error message, or the empty string for success.
+
+=cut
+
+sub new_customer {
+  my $hashref = shift;
+
+  socket(SOCK, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!";
+  connect(SOCK, sockaddr_un($fs_signupd_socket)) or die "connect: $!";
+  print SOCK "new_customer\n";
+
+  my $signup_data = { map { $_ => $hashref->{$_} } qw(
+    first last ss company address1 address2 city county state zip country
+    daytime night fax payby payinfo paydate payname invoicing_list
+    referral_custnum comments pkgpart username _password sec_phrase popnum
+  ) };
+
+  $signup_data->{agentnum} = $hashref->{agentnum} if $hashref->{agentnum};
+
+  nstore_fd($signup_data, \*SOCK) or die "can't send customer signup: $!";
+  SOCK->flush;
+
+  chop( my $error = <SOCK> );
+  $error;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<fs_signupd>, L<FS::cust_main>
+
+=cut
+
+1;
+
diff --git a/fs_signup/FS-SignupClient/cgi/decline.html b/fs_signup/FS-SignupClient/cgi/decline.html
new file mode 100644 (file)
index 0000000..a37ba3a
--- /dev/null
@@ -0,0 +1,5 @@
+<HTML><HEAD><TITLE>Processing error</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Processing error</FONT><BR><BR>
+There has been an error processing your account.  Please contact customer
+support.
+</BODY></HTML>
diff --git a/fs_signup/FS-SignupClient/cgi/signup-alternate.html b/fs_signup/FS-SignupClient/cgi/signup-alternate.html
new file mode 100755 (executable)
index 0000000..490cefa
--- /dev/null
@@ -0,0 +1,218 @@
+<HTML><HEAD><TITLE>ISP Signup form</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>ISP Signup form</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+<FORM NAME="dummy">
+<INPUT TYPE="hidden" NAME="magic" VALUE="process">
+<INPUT TYPE="hidden" NAME="ref" VALUE="<%= $referral_custnum %>">
+<INPUT TYPE="hidden" NAME="ss" VALUE="">
+<INPUT TYPE="hidden" NAME="agentnum" VALUE="3">
+Contact Information
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Contact name<BR>(last, first)</TH>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="last" VALUE="<%= $last %>">,
+                <INPUT TYPE="text" NAME="first" VALUE="<%= $first %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Company</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="company" SIZE=70 VALUE="<%= $company %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Address</TH>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="address1" SIZE=70 VALUE="<%= $address1 %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">&nbsp;</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="address2" SIZE=70 VALUE="<%= $address2 %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>City</TH>
+  <TD><INPUT TYPE="text" NAME="city" VALUE="<%= $city %>"></TD>
+  <TH ALIGN="right"><font color="#ff0000">*</font>State/Country</TH>
+  <TD><SELECT NAME="state" SIZE="1">
+
+  <%=
+    foreach ( @{$locales} ) {
+      my $value = $_->{'state'};
+      $value .= ' ('. $_->{'county'}. ')' if $_->{'county'};
+      $value .= ' / '. $_->{'country'};
+
+      $OUT .= qq(<OPTION VALUE="$value");
+      $OUT .= ' SELECTED' if ( $state eq $_->{'state'}
+                               && $county eq $_->{'county'}
+                               && $country eq $_->{'country'}
+                             );
+      $OUT .= ">$value</OPTION>";
+    }
+  %>
+
+  </SELECT></TD>
+  <TH><font color="#ff0000">*</font>Zip</TH>
+  <TD><INPUT TYPE="text" NAME="zip" SIZE=10 VALUE="<%= $zip %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Day Phone</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="daytime" VALUE="<%= $daytime %>" SIZE=18></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Night Phone</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="night" VALUE="<%= $night %>" SIZE=18></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Fax</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="fax" VALUE="<%= $fax %>" SIZE=12></TD>
+</TR>
+</TABLE><font color="#ff0000">*</font> required fields<BR>
+
+<BR><BR>
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Username</TH>
+  <TD><INPUT TYPE="text" NAME="username" VALUE="<%= $username %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Password</TH>
+  <TD><INPUT TYPE="password" NAME="_password" VALUE="<%= $password %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Re-enter Password</TH>
+  <TD><INPUT TYPE="password" NAME="_password2" VALUE="<%= $password2 %>"></TD>
+</TR>
+
+<%= if ( $init_data->{'security_phrase'} ) {
+      <<ENDOUT;
+<TR>
+  <TD ALIGN="right">Security Phrase</TD>
+  <TD><INPUT TYPE="text" NAME="sec_phrase" VALUE="$sec_phrase">
+  </TD>
+</TR>
+ENDOUT
+    } else {
+      '<INPUT TYPE="hidden" NAME="sec_phrase" VALUE="">';
+    }
+%>
+
+<%= if ( scalar(@$pops) ) {
+      '<TR><TD ALIGN="right">Access number</TD><TD>'.
+           popselector($popnum). '</TD></TR>';
+    } else {
+      popselector($popnum);
+    }
+%>
+
+</TABLE><font color="#ff0000">*</font> required fields
+
+<BR><BR>First package
+
+  <%= use Tie::IxHash;
+      my %pkgpart2payby = map { $_->{pkgpart} => $_->{payby}[0] } @{$packages};
+      tie my %options, 'Tie::IxHash',
+        '' => '(none)',
+        map { $_->{pkgpart} => $_->{pkg} }
+          sort { $a->{recur} <=> $b->{recur} }
+            @{$packages} 
+      ;
+
+      use HTML::Widgets::SelectLayers 0.02;
+      my @form_text = qw( magic ref ss agentnum
+                          last first company address1 address2
+                          city zip daytime night fax
+                          username _password _password2 sec_phrase );
+      my @form_select = qw( state ); #county country
+      if ( scalar(@$pops) == 0 or scalar(@$pops) == 1 ) {
+        push @form_text, 'popnum',
+      } else {
+        push @form_select, 'popnum',
+      }
+      my $widget = new HTML::Widgets::SelectLayers(
+        options => \%options,
+        selected_layer => $pkgpart,
+        form_name => 'dummy',
+        form_action => $self_url,
+        form_text => \@form_text,
+        form_select => \@form_select,
+        layer_callback => sub {
+          my $layer = shift;
+          my $html = qq( <INPUT TYPE="hidden" NAME="pkgpart" VALUE="$layer">);
+
+          if ( $pkgpart2payby{$layer} eq 'BILL' ) {
+            $html .= <<ENDOUT;
+<INPUT TYPE="hidden" NAME="payby" VALUE="BILL">
+<INPUT TYPE="hidden" NAME="invoicing_list_POST" VALUE="">
+<INPUT TYPE="hidden" NAME="BILL_payinfo" VALUE="">
+<INPUT TYPE="hidden" NAME="BILL_month" VALUE="12">
+<INPUT TYPE="hidden" NAME="BILL_year" VALUE="2037">
+<INPUT TYPE="hidden" NAME="BILL_payname" VALUE="">
+<BR><BR><INPUT TYPE="submit" VALUE="Signup">
+ENDOUT
+          } elsif ( $pkgpart2payby{$layer} eq 'CARD' ) {
+            my $postal_checked = '';
+            my @invoicing_list = split(', ', $invoicing_list );
+            $postal_checked = 'CHECKED'
+              if ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list;
+
+            $invoicing_list= join(', ', grep { $_ ne 'POST' } @invoicing_list );
+
+            my $expselect = expselect("CARD", $paydate);
+
+            my $cardselect = '<SELECT NAME="CARD_type"><OPTION></OPTION>';
+            my %types = (
+                          'VISA' => 'VISA card',
+                          'MasterCard' => 'MasterCard',
+                          'Discover' => 'Discover card',
+                          'American Express' => 'American Express card',
+                        );
+            foreach ( keys %types ) {
+              $selected =
+                $cgi->param('CARD_type') eq $types{$_} ? 'SELECTED' : '';
+              $cardselect .=
+                qq!<OPTION $selected VALUE="$types{$_}">$_</OPTION>!;
+            }
+            $cardselect .= '</SELECT>';
+
+            $html .= <<ENDOUT;
+<INPUT TYPE="hidden" NAME="payby" VALUE="CARD">
+<BR><BR>Billing information
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0>
+<INPUT TYPE="hidden" NAME="invoicing_list_POST" VALUE="">
+<TR>
+  <TD ALIGN="right">Email statement to </TD>
+  <TD><INPUT TYPE="text" NAME="invoicing_list" VALUE="$invoicing_list"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Credit card type</TH>
+  <TD>$cardselect</TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Card number</TH>
+  <TD><INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>*</font>Exp</TH>
+  <TD>$expselect</TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Name on card</TH>
+  <TD><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname"></TD>
+</TR>
+</TABLE>
+<font color="#ff0000">*</font> required fields
+<BR><BR><INPUT TYPE="submit" VALUE="Signup">
+ENDOUT
+          } else {
+            $html = <<ENDOUT;
+<BR>Please select a package.<BR>
+ENDOUT
+
+          }
+
+          $html;
+
+        },
+      );
+
+      $widget->html;
+
+
+ %>
+</BODY></HTML>
diff --git a/fs_signup/FS-SignupClient/cgi/signup.cgi b/fs_signup/FS-SignupClient/cgi/signup.cgi
new file mode 100755 (executable)
index 0000000..57b93d4
--- /dev/null
@@ -0,0 +1,664 @@
+#!/usr/bin/perl -Tw
+#
+# $Id: signup.cgi,v 1.43 2003-07-04 03:21:42 ivan Exp $
+
+use strict;
+use vars qw( @payby $cgi $locales $packages
+             $pops %pop %popnum2pop
+             $init_data $error
+             $last $first $ss $company $address1 $address2 $city $state $county
+             $country $zip $daytime $night $fax $invoicing_list $payby $payinfo
+             $paydate $payname $referral_custnum $init_popstate
+             $pkgpart $username $password $password2 $sec_phrase $popnum
+             $agentnum
+             $ieak_file $ieak_template $cck_file $cck_template
+             $signup_html $signup_template
+             $success_html $success_template
+             $decline_html $decline_template
+             $ac $exch $loc
+             $email_name $pkg
+             $self_url
+           );
+use subs qw( print_form print_okay print_decline
+             success_default decline_default
+             expselect );
+use CGI;
+#use CGI::Carp qw(fatalsToBrowser);
+use Text::Template;
+use Business::CreditCard;
+use HTTP::Headers::UserAgent 2.00;
+use FS::SignupClient 0.03 qw( signup_info new_customer );
+
+#acceptable payment methods
+#
+#@payby = qw( CARD BILL COMP );
+#@payby = qw( CARD BILL );
+#@payby = qw( CARD );
+@payby = qw( CARD PREPAY );
+
+$ieak_file = '/usr/local/freeside/ieak.template';
+$cck_file = '/usr/local/freeside/cck.template';
+$signup_html = -e 'signup.html'
+                 ? 'signup.html'
+                 : '/usr/local/freeside/signup.html';
+$success_html = -e 'success.html'
+                  ? 'success.html'
+                  : '/usr/local/freeside/success.html';
+$decline_html = -e 'decline.html'
+                  ? 'decline.html'
+                  : '/usr/local/freeside/decline.html';
+
+
+if ( -e $ieak_file ) {
+  my $ieak_txt = Text::Template::_load_text($ieak_file)
+    or die $Text::Template::ERROR;
+  $ieak_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
+  $ieak_txt = $1;
+  $ieak_txt =~ s/\r//g; # don't double \r on old templates
+  $ieak_txt =~ s/\n/\r\n/g;
+  $ieak_template = new Text::Template ( TYPE => 'STRING', SOURCE => $ieak_txt )
+    or die $Text::Template::ERROR;
+} else {
+  $ieak_template = '';
+}
+
+if ( -e $cck_file ) {
+  my $cck_txt = Text::Template::_load_text($cck_file)
+    or die $Text::Template::ERROR;
+  $cck_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
+  $cck_txt = $1;
+  $cck_template = new Text::Template ( TYPE => 'STRING', SOURCE => $cck_txt )
+    or die $Text::Template::ERROR;
+} else {
+  $cck_template = '';
+}
+
+$agentnum = '';
+if ( -e $signup_html ) {
+  my $signup_txt = Text::Template::_load_text($signup_html)
+    or die $Text::Template::ERROR;
+  $signup_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
+  $signup_txt = $1;
+  $signup_template = new Text::Template ( TYPE => 'STRING',
+                                          SOURCE => $signup_txt,
+                                          DELIMITERS => [ '<%=', '%>' ]
+                                        )
+    or die $Text::Template::ERROR;
+  if ( $signup_txt =~
+         /<\s*INPUT TYPE="?hidden"?\s+NAME="?agentnum"?\s+VALUE="?(\d+)"?\s*>/si
+  ) {
+    $agentnum = $1;
+  }
+} else {
+  #too much maintenance hassle to keep in this file
+  die "can't find ./signup.html or /usr/local/freeside/signup.html";
+  #$signup_template = new Text::Template ( TYPE => 'STRING',
+  #                                        SOURCE => &signup_default,
+  #                                        DELIMITERS => [ '<%=', '%>' ]
+  #                                      )
+  #  or die $Text::Template::ERROR;
+}
+
+if ( -e $success_html ) {
+  my $success_txt = Text::Template::_load_text($success_html)
+    or die $Text::Template::ERROR;
+  $success_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
+  $success_txt = $1;
+  $success_template = new Text::Template ( TYPE => 'STRING',
+                                           SOURCE => $success_txt,
+                                           DELIMITERS => [ '<%=', '%>' ],
+                                         )
+    or die $Text::Template::ERROR;
+} else {
+  $success_template = new Text::Template ( TYPE => 'STRING',
+                                           SOURCE => &success_default,
+                                           DELIMITERS => [ '<%=', '%>' ],
+                                         )
+    or die $Text::Template::ERROR;
+}
+
+if ( -e $decline_html ) {
+  my $decline_txt = Text::Template::_load_text($decline_html)
+    or die $Text::Template::ERROR;
+  $decline_txt =~ /^(.*)$/s; #untaint the template source - it's trusted
+  $decline_txt = $1;
+  $decline_template = new Text::Template ( TYPE => 'STRING',
+                                           SOURCE => $decline_txt,
+                                           DELIMITERS => [ '<%=', '%>' ],
+                                         )
+    or die $Text::Template::ERROR;
+} else {
+  $decline_template = new Text::Template ( TYPE => 'STRING',
+                                           SOURCE => &decline_default,
+                                           DELIMITERS => [ '<%=', '%>' ],
+                                         )
+    or die $Text::Template::ERROR;
+}
+
+
+( $locales, $packages, $pops, $init_data ) = signup_info();
+@payby = @{$init_data->{'payby'}} if @{$init_data->{'payby'}};
+$packages = $init_data->{agentnum2part_pkg}{$agentnum} if $agentnum;
+%pop = ();
+%popnum2pop = ();
+foreach (@$pops) {
+  push @{ $pop{ $_->{state} }->{ $_->{ac} } }, $_;
+  $popnum2pop{$_->{popnum}} = $_;
+}
+
+$cgi = new CGI;
+
+if ( defined $cgi->param('magic') ) {
+  if ( $cgi->param('magic') eq 'process' ) {
+
+    if ( $cgi->param('state') =~ /^(\w*)( \(([\w ]+)\))? ?\/ ?(\w+)$/ ) {
+      $state = $1;
+      $county = $3 || '';
+      $country = $4;
+    } elsif ( $cgi->param('state') =~ /^(\w*)$/ ) {
+      $state = $1;
+      $cgi->param('county') =~ /^([\w ]*)$/
+        or die "illegal county: ". $cgi->param('county');
+      $county = $1;
+      $cgi->param('country') =~ /^(\w+)$/
+        or die "illegal country: ". $cgi->param('country');
+      $country = $1;
+    } else {
+      die "illegal state: ". $cgi->param('state');
+    }
+
+    $payby = $cgi->param('payby');
+    if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) {
+      #$payinfo = join('@', map { $cgi->param( $payby. "_payinfo$_" ) } (1,2) );
+      $payinfo = $cgi->param($payby. '_payinfo1'). '@'. 
+                 $cgi->param($payby. '_payinfo2');
+    } else {
+      $payinfo = $cgi->param( $payby. '_payinfo' );
+    }
+    $paydate =
+      $cgi->param( $payby. '_month' ). '-'. $cgi->param( $payby. '_year' );
+    $payname = $cgi->param( $payby. '_payname' );
+
+    if ( $invoicing_list = $cgi->param('invoicing_list') ) {
+      $invoicing_list .= ', POST' if $cgi->param('invoicing_list_POST');
+    } else {
+      $invoicing_list = 'POST';
+    }
+
+    $error = '';
+
+    $last             = $cgi->param('last');
+    $first            = $cgi->param('first');
+    $ss               = $cgi->param('ss');
+    $company          = $cgi->param('company');
+    $address1         = $cgi->param('address1');
+    $address2         = $cgi->param('address2');
+    $city             = $cgi->param('city');
+    #$county,
+    #$state,
+    $zip              = $cgi->param('zip');
+    #$country,
+    $daytime          = $cgi->param('daytime');
+    $night            = $cgi->param('night');
+    $fax              = $cgi->param('fax');
+    #$payby,
+    #$payinfo,
+    #$paydate,
+    #$payname,
+    #$invoicing_list,
+    $referral_custnum = $cgi->param('ref');
+    $pkgpart          = $cgi->param('pkgpart');
+    $username         = $cgi->param('username');
+    $sec_phrase       = $cgi->param('sec_phrase');
+    $password         = $cgi->param('_password');
+    $popnum           = $cgi->param('popnum');
+    #$agentnum, #         = $cgi->param('agentnum'),
+    $init_popstate    = $cgi->param('init_popstate');
+
+    if ( $cgi->param('_password') ne $cgi->param('_password2') ) {
+      $error = $init_data->{msgcat}{passwords_dont_match}; #msgcat
+      $password  = '';
+      $password2 = '';
+    } else {
+      $password2 = $cgi->param('_password2');
+
+      if ( $payby =~ /^(CARD|DCRD)$/ && $cgi->param('CARD_type') ) {
+        $payinfo =~ s/\D//g;
+
+        $payinfo =~ /^(\d{13,16})$/
+          or $error ||= $init_data->{msgcat}{invalid_card}; #. $self->payinfo;
+        $payinfo = $1;
+        validate($payinfo)
+          or $error ||= $init_data->{msgcat}{invalid_card}; #. $self->payinfo;
+        cardtype($payinfo) eq $cgi->param('CARD_type')
+          or $error ||= $init_data->{msgcat}{not_a}. $cgi->param('CARD_type');
+      }
+
+      $error ||= new_customer ( {
+        'last'             => $last,
+        'first'            => $first,
+        'ss'               => $ss,
+        'company'          => $company,
+        'address1'         => $address1,
+        'address2'         => $address2,
+        'city'             => $city,
+        'county'           => $county,
+        'state'            => $state,
+        'zip'              => $zip,
+        'country'          => $country,
+        'daytime'          => $daytime,
+        'night'            => $night,
+        'fax'              => $fax,
+        'payby'            => $payby,
+        'payinfo'          => $payinfo,
+        'paydate'          => $paydate,
+        'payname'          => $payname,
+        'invoicing_list'   => $invoicing_list,
+        'referral_custnum' => $referral_custnum,
+        'pkgpart'          => $pkgpart,
+        'username'         => $username,
+        'sec_phrase'       => $sec_phrase,
+        '_password'        => $password,
+        'popnum'           => $popnum,
+        'agentnum'         => $agentnum,
+      } );
+
+    }
+    
+    if ( $error eq '_decline' ) {
+      print_decline();
+    } elsif ( $error ) {
+      print_form();
+    } else {
+      print_okay();
+    }
+
+  } else {
+    die "unrecognized magic: ". $cgi->param('magic');
+  }
+} else {
+  $error = '';
+  $last = '';
+  $first = '';
+  $ss = '';
+  $company = '';
+  $address1 = '';
+  $address2 = '';
+  $city = '';
+  $state = $init_data->{statedefault};
+  $county = '';
+  $country = $init_data->{countrydefault};
+  $zip = '';
+  $daytime = '';
+  $night = '';
+  $fax = '';
+  $invoicing_list = '';
+  $payby = '';
+  $payinfo = '';
+  $paydate = '';
+  $payname = '';
+  $pkgpart = '';
+  $username = '';
+  $password = '';
+  $password2 = '';
+  $sec_phrase = '';
+  $popnum = '';
+  $referral_custnum = $cgi->param('ref') || '';
+  $init_popstate = $cgi->param('init_popstate') || '';
+  print_form;
+}
+
+sub print_form {
+
+  $cgi->delete('ref');
+  $cgi->delete('init_popstate');
+  $self_url = $cgi->self_url;
+
+  $error = "Error: $error" if $error;
+
+  print $cgi->header( '-expires' => 'now' ),
+        $signup_template->fill_in();
+
+}
+
+sub print_decline {
+  print $cgi->header( '-expires' => 'now' ),
+        $decline_template->fill_in();
+}
+
+sub print_okay {
+  my $user_agent = new HTTP::Headers::UserAgent $ENV{HTTP_USER_AGENT};
+
+  $cgi->param('username') =~ /^(.+)$/
+    or die "fatal: invalid username got past FS::SignupClient::new_customer";
+  my $username = $1;
+  $cgi->param('_password') =~ /^(.+)$/
+    or die "fatal: invalid password got past FS::SignupClient::new_customer";
+  my $password = $1;
+  ( $cgi->param('first'). ' '. $cgi->param('last') ) =~ /^(.*)$/
+    or die "fatal: invalid email_name got past FS::SignupClient::new_customer";
+  $email_name = $1; #global for template
+
+  my $pop = $popnum2pop{$cgi->param('popnum')};
+    #or die "fatal: invalid popnum got past FS::SignupClient::new_customer";
+  if ( $pop ) {
+    ( $ac, $exch, $loc ) = ( $pop->{'ac'}, $pop->{'exch'}, $pop->{'loc'} );
+  } else {
+    ( $ac, $exch, $loc ) = ( '', '', ''); #presumably you're not using them.
+  }
+
+  #global for template
+  $pkg = ( grep { $_->{'pkgpart'} eq $pkgpart } @$packages )[0]->{'pkg'};
+
+  if ( $ieak_template
+       && $user_agent->platform eq 'ia32'
+       && $user_agent->os =~ /^win/
+       && ($user_agent->browser)[0] eq 'IE'
+     )
+  { #send an IEAK config
+    print $cgi->header('application/x-Internet-signup'),
+          $ieak_template->fill_in();
+  } elsif ( $cck_template
+            && $user_agent->platform eq 'ia32'
+            && $user_agent->os =~ /^win/
+            && ($user_agent->browser)[0] eq 'Netscape'
+          )
+  { #send a Netscape config
+    my $cck_data = $cck_template->fill_in();
+    print $cgi->header('application/x-netscape-autoconfigure-dialer-v2'),
+          map {
+            m/(.*)\s+(.*)$/;
+            pack("N", length($1)). $1. pack("N", length($2)). $2;
+          } split(/\n/, $cck_data);
+
+  } else { #send a simple confirmation
+    print $cgi->header( '-expires' => 'now' ),
+          $success_template->fill_in();
+  }
+}
+
+#horrible false laziness with FS/FS/svc_acct_pop.pm::popselector
+sub popselector {
+
+  my( $popnum ) = @_;
+
+  return '<INPUT TYPE="hidden" NAME="popnum" VALUE="">' unless @$pops;
+  return $pops->[0]{city}. ', '. $pops->[0]{state}.
+         ' ('. $pops->[0]{ac}. ')/'. $pops->[0]{exch}. '-'. $pops->[0]{loc}.
+         '<INPUT TYPE="hidden" NAME="popnum" VALUE="'. $pops->[0]{popnum}. '">'
+    if scalar(@$pops) == 1;
+
+  #my %pop = ();
+  #my %popnum2pop = ();
+  #foreach (@$pops) {
+  #  push @{ $pop{ $_->{state} }->{ $_->{ac} } }, $_;
+  #  $popnum2pop{$_->{popnum}} = $_;
+  #}
+
+  my $text = <<END;
+    <SCRIPT>
+    function opt(what,href,text) {
+      var optionName = new Option(text, href, false, false)
+      var length = what.length;
+      what.options[length] = optionName;
+    }
+END
+
+  if ( $init_popstate ) {
+    $text .= '<INPUT TYPE="hidden" NAME="init_popstate" VALUE="'.
+             $init_popstate. '">';
+  } else {
+    $text .= <<END;
+      function acstate_changed(what) {
+        state = what.options[what.selectedIndex].text;
+        what.form.popac.options.length = 0
+        what.form.popac.options[0] = new Option("Area code", "-1", false, true);
+END
+  } 
+
+  my @states = $init_popstate ? ( $init_popstate ) : keys %pop;
+  foreach my $state ( sort { $a cmp $b } @states ) {
+    $text .= "\nif ( state == \"$state\" ) {\n" unless $init_popstate;
+
+    foreach my $ac ( sort { $a cmp $b } keys %{ $pop{$state} }) {
+      $text .= "opt(what.form.popac, \"$ac\", \"$ac\");\n";
+      if ($ac eq $cgi->param('popac')) {
+        $text .= "what.form.popac.options[what.form.popac.length-1].selected = true;\n";
+      }
+    }
+    $text .= "}\n" unless $init_popstate;
+  }
+  $text .= "popac_changed(what.form.popac)}\n";
+
+  $text .= <<END;
+  function popac_changed(what) {
+    ac = what.options[what.selectedIndex].text;
+    what.form.popnum.options.length = 0;
+    what.form.popnum.options[0] = new Option("City", "-1", false, true);
+
+END
+
+  foreach my $state ( @states ) {
+    foreach my $popac ( keys %{ $pop{$state} } ) {
+      $text .= "\nif ( ac == \"$popac\" ) {\n";
+
+      foreach my $pop ( @{$pop{$state}->{$popac}}) {
+        my $o_popnum = $pop->{popnum};
+        my $poptext =  $pop->{city}. ', '. $pop->{state}.
+                       ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
+
+        $text .= "opt(what.form.popnum, \"$o_popnum\", \"$poptext\");\n";
+        if ($popnum == $o_popnum) {
+          $text .= "what.form.popnum.options[what.form.popnum.length-1].selected = true;\n";
+        }
+      }
+      $text .= "}\n";
+    }
+  }
+
+
+  $text .= "}\n</SCRIPT>\n";
+
+  $text .=
+    qq!<TABLE CELLPADDING="0"><TR><TD><SELECT NAME="acstate"! .
+    qq!SIZE=1 onChange="acstate_changed(this)"><OPTION VALUE=-1>State!;
+  $text .= "<OPTION" . ($_ eq $cgi->param('acstate') ? " SELECTED" : "") .
+           ">$_" foreach sort { $a cmp $b } @states;
+  $text .= '</SELECT>'; #callback? return 3 html pieces?  #'</TD>';
+
+  $text .=
+    qq!<SELECT NAME="popac" SIZE=1 onChange="popac_changed(this)">!.
+    qq!<OPTION>Area code</SELECT></TR><TR VALIGN="top">!;
+
+  $text .= qq!<TR><TD><SELECT NAME="popnum" SIZE=1 STYLE="width: 20em"><OPTION>City!;
+
+
+  #comment this block to disable initial list polulation
+  my @initial_select = ();
+  if ( scalar( @$pops ) > 100 ) {
+    push @initial_select, $popnum2pop{$popnum} if $popnum2pop{$popnum};
+  } else {
+    @initial_select = @$pops;
+  }
+  foreach my $pop ( sort { $a->{state} cmp $b->{state} } @initial_select ) {
+    $text .= qq!<OPTION VALUE="!. $pop->{popnum}. '"'.
+             ( ( $popnum && $pop->{popnum} == $popnum ) ? ' SELECTED' : '' ). ">".
+             $pop->{city}. ', '. $pop->{state}.
+               ' ('. $pop->{ac}. ')/'. $pop->{exch}. '-'. $pop->{loc};
+  }
+
+  $text .= qq!</SELECT></TD></TR></TABLE>!;
+
+  $text;
+
+}
+
+sub expselect {
+  my $prefix = shift;
+  my $date = shift || '';
+  my( $m, $y ) = ( 0, 0 );
+  if ( $date  =~ /^(\d{4})-(\d{2})-\d{2}$/ ) { #PostgreSQL date format
+    ( $m, $y ) = ( $2, $1 );
+  } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+    ( $m, $y ) = ( $1, $3 );
+  }
+  my $return = qq!<SELECT NAME="$prefix!. qq!_month" SIZE="1">!;
+  for ( 1 .. 12 ) {
+    $return .= "<OPTION";
+    $return .= " SELECTED" if $_ == $m;
+    $return .= ">$_";
+  }
+  $return .= qq!</SELECT>/<SELECT NAME="$prefix!. qq!_year" SIZE="1">!;
+  for ( 2001 .. 2037 ) {
+    $return .= "<OPTION";
+    $return .= " SELECTED" if $_ == $y;
+    $return .= ">$_";
+  }
+  $return .= "</SELECT>";
+
+  $return;
+}
+
+#false laziness w/FS::cust_main_county
+sub regionselector {
+  my ( $selected_county, $selected_state, $selected_country,
+       $prefix, $onchange ) = @_;
+
+  my $prefix = '' unless defined $prefix;
+
+  my $countyflag = 0;
+
+  my %cust_main_county;
+
+#  unless ( @cust_main_county ) { #cache 
+    #@cust_main_county = qsearch('cust_main_county', {} );
+    #foreach my $c ( @cust_main_county ) {
+    foreach my $c ( @$locales ) {
+      #$countyflag=1 if $c->county;
+      $countyflag=1 if $c->{county};
+      #push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
+      #$cust_main_county{$c->country}{$c->state}{$c->county} = 1;
+      $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}} = 1;
+    }
+#  }
+  $countyflag=1 if $selected_county;
+
+  my $script_html = <<END;
+    <SCRIPT>
+    function opt(what,value,text) {
+      var optionName = new Option(text, value, false, false);
+      var length = what.length;
+      what.options[length] = optionName;
+    }
+    function ${prefix}country_changed(what) {
+      country = what.options[what.selectedIndex].text;
+      for ( var i = what.form.${prefix}state.length; i >= 0; i-- )
+          what.form.${prefix}state.options[i] = null;
+END
+      #what.form.${prefix}state.options[0] = new Option('', '', false, true);
+
+  foreach my $country ( sort keys %cust_main_county ) {
+    $script_html .= "\nif ( country == \"$country\" ) {\n";
+    foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
+      my $text = $state || '(n/a)';
+      $script_html .= qq!opt(what.form.${prefix}state, "$state", "$text");\n!;
+    }
+    $script_html .= "}\n";
+  }
+
+  $script_html .= <<END;
+    }
+    function ${prefix}state_changed(what) {
+END
+
+  if ( $countyflag ) {
+    $script_html .= <<END;
+      state = what.options[what.selectedIndex].text;
+      country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
+      for ( var i = what.form.${prefix}county.length; i >= 0; i-- )
+          what.form.${prefix}county.options[i] = null;
+END
+
+    foreach my $country ( sort keys %cust_main_county ) {
+      $script_html .= "\nif ( country == \"$country\" ) {\n";
+      foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
+        $script_html .= "\nif ( state == \"$state\" ) {\n";
+          #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
+          foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
+            my $text = $county || '(n/a)';
+            $script_html .=
+              qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
+          }
+        $script_html .= "}\n";
+      }
+      $script_html .= "}\n";
+    }
+  }
+
+  $script_html .= <<END;
+    }
+    </SCRIPT>
+END
+
+  my $county_html = $script_html;
+  if ( $countyflag ) {
+    $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$onchange">!;
+    $county_html .= '</SELECT>';
+  } else {
+    $county_html .=
+      qq!<INPUT TYPE="hidden" NAME="${prefix}county" VALUE="$selected_county">!;
+  }
+
+  my $state_html = qq!<SELECT NAME="${prefix}state" !.
+                   qq!onChange="${prefix}state_changed(this); $onchange">!;
+  foreach my $state ( sort keys %{ $cust_main_county{$selected_country} } ) {
+    my $text = $state || '(n/a)';
+    my $selected = $state eq $selected_state ? 'SELECTED' : '';
+    $state_html .= "\n<OPTION $selected VALUE=$state>$text</OPTION>"
+  }
+  $state_html .= '</SELECT>';
+
+  $state_html .= '</SELECT>';
+
+  my $country_html = qq!<SELECT NAME="${prefix}country" !.
+                     qq!onChange="${prefix}country_changed(this); $onchange">!;
+  my $countrydefault = $init_data->{countrydefault} || 'US';
+  foreach my $country (
+    sort { ($b eq $countrydefault) <=> ($a eq $countrydefault) or $a cmp $b }
+      keys %cust_main_county
+  ) {
+    my $selected = $country eq $selected_country ? ' SELECTED' : '';
+    $country_html .= "\n<OPTION$selected>$country</OPTION>"
+  }
+  $country_html .= '</SELECT>';
+
+  ($county_html, $state_html, $country_html);
+
+}
+
+sub success_default { #html to use if you don't specify a success file
+  <<'END';
+<HTML><HEAD><TITLE>Signup successful</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Signup successful</FONT><BR><BR>
+Thanks for signing up!
+<BR><BR>
+Signup information for <%= $email_name %>:
+<BR><BR>
+Username: <%= $username %><BR>
+Password: <%= $password %><BR>
+Access number: (<%= $ac %>) / $exch - $local<BR>
+Package: <%= $pkg %><BR>
+</BODY></HTML>
+END
+}
+
+sub decline_default { #html to use if there is a decline
+  <<'END';
+<HTML><HEAD><TITLE>Processing error</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Processing error</FONT><BR><BR>
+There has been an error processing your account.  Please contact customer
+support.
+</BODY></HTML>
+END
+}
+
diff --git a/fs_signup/FS-SignupClient/cgi/signup.html b/fs_signup/FS-SignupClient/cgi/signup.html
new file mode 100755 (executable)
index 0000000..8077409
--- /dev/null
@@ -0,0 +1,184 @@
+<HTML><HEAD><TITLE>ISP Signup form</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>ISP Signup form</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+<FORM ACTION="<%= $self_url %>" METHOD=POST>
+<INPUT TYPE="hidden" NAME="magic" VALUE="process">
+<INPUT TYPE="hidden" NAME="ref" VALUE="<%= $referral_custnum %>">
+<INPUT TYPE="hidden" NAME="ss" VALUE="">
+Contact Information
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Contact name<BR>(last, first)</TH>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="last" VALUE="<%= $last %>">,
+                <INPUT TYPE="text" NAME="first" VALUE="<%= $first %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Company</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="company" SIZE=70 VALUE="<%= $company %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Address</TH>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="address1" SIZE=70 VALUE="<%= $address1 %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">&nbsp;</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="address2" SIZE=70 VALUE="<%= $address2 %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>City</TH>
+  <TD><INPUT TYPE="text" NAME="city" VALUE="<%= $city %>"></TD>
+  <TH ALIGN="right"><font color="#ff0000">*</font>State/Country</TH>
+  <TD>
+    <%=
+        ($county_html, $state_html, $country_html) =
+          regionselector( $county, $state, $country );
+        "$county_html $state_html";
+    %>
+  </TD>
+  <TH><font color="#ff0000">*</font>Zip</TH>
+  <TD><INPUT TYPE="text" NAME="zip" SIZE=10 VALUE="<%= $zip %>"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right"><font color="#ff0000">*</font>Country</TH>
+  <TD><%= $country_html %></TD>
+<TR>
+  <TD ALIGN="right">Day Phone</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="daytime" VALUE="<%= $daytime %>" SIZE=18></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Night Phone</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="night" VALUE="<%= $night %>" SIZE=18></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Fax</TD>
+  <TD COLSPAN=5><INPUT TYPE="text" NAME="fax" VALUE="<%= $fax %>" SIZE=12></TD>
+</TR>
+</TABLE><font color="#ff0000">*</font> required fields<BR>
+<BR>Billing information<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR><TD>
+
+  <%=
+    $OUT .= '<INPUT TYPE="checkbox" NAME="invoicing_list_POST" VALUE="POST"';
+    my @invoicing_list = split(', ', $invoicing_list );
+    $OUT .= ' CHECKED'
+      if ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list;
+    $OUT .= '>';
+  %>
+
+  Postal mail invoice
+</TD></TR>
+<TR><TD>Email invoice <INPUT TYPE="text" NAME="invoicing_list" VALUE="<%= join(', ', grep { $_ ne 'POST' } split(', ', $invoicing_list ) ) %>">
+</TD></TR>
+<%= scalar(@payby) > 1 ? '<TR><TD>Billing type</TD></TR>' : '' %>
+</TABLE>
+<TABLE BGCOLOR="#c0c0c0" BORDER=1 WIDTH="100%">
+<TR>
+
+  <%=
+
+    my $cardselect = '<SELECT NAME="CARD_type"><OPTION></OPTION>';
+    my %types = (
+                  'VISA' => 'VISA card',
+                  'MasterCard' => 'MasterCard',
+                  'Discover' => 'Discover card',
+                  'American Express' => 'American Express card',
+                );
+    foreach ( keys %types ) {
+      $selected = $cgi->param('CARD_type') eq $types{$_} ? 'SELECTED' : '';
+      $cardselect .= qq!<OPTION $selected VALUE="$types{$_}">$_</OPTION>!;
+    }
+    $cardselect .= '</SELECT>';
+  
+    my %payby = (
+      'CARD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("CARD"). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="">!,
+      'DCRD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("DCRD"). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="">!,
+      'CHEK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="">!,
+      'DCHK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="">!,
+      'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
+      'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE=""><BR><font color="#ff0000">*</font>Exp !. expselect("BILL", "12-2037"). qq!<BR><font color="#ff0000">*</font>Attention<BR><INPUT TYPE="text" NAME="BILL_payname" VALUE="Accounts Payable">!,
+      'COMP' => qq!Complimentary<BR><font color="#ff0000">*</font>Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE=""><BR><font color="#ff0000">*</font>Exp !. expselect("COMP"),
+      'PREPAY' => qq!Prepaid card<BR><font color="#ff0000">*</font><INPUT TYPE="text" NAME="PREPAY_payinfo" VALUE="" MAXLENGTH=80>!,
+    );
+
+    my( $account, $aba ) = split('@', $payinfo);
+    my %paybychecked = (
+      'CARD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("CARD", $paydate). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname">!,
+      'DCRD' => qq!Credit card<BR><font color="#ff0000">*</font>$cardselect<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR><font color="#ff0000">*</font>Exp !. expselect("DCRD", $paydate). qq!<BR><font color="#ff0000">*</font>Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="$payname">!,
+      'CHEK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="$account" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="$payname">!,
+      'DCHK' => qq!Electronic check<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="$account" MAXLENGTH=10><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="$payname">!,
+      'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="$payinfo" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
+      'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE="$payinfo"><BR><font color="#ff0000">*</font>Exp !. expselect("BILL", $paydate). qq!<BR><font color="#ff0000">*</font>Attention<BR><INPUT TYPE="text" NAME="BILL_payname" VALUE="$payname">!,
+      'COMP' => qq!Complimentary<BR><font color="#ff0000">*</font>Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE="$payinfo"><BR><font color="#ff0000">*</font>Exp !. expselect("COMP", $paydate),
+      'PREPAY' => qq!Prepaid card<BR><font color="#ff0000">*</font><INPUT TYPE="text" NAME="PREPAY_payinfo" VALUE="$payinfo" MAXLENGTH=80>!,
+    );
+
+    for (@payby) {
+      if ( scalar(@payby) == 1) {
+        $OUT .= '<TD VALIGN=TOP>'.
+                qq!<INPUT TYPE="hidden" NAME="payby" VALUE="$_">!.
+                "$paybychecked{$_}</TD>";
+      } else {
+        $OUT .= qq!<TD VALIGN=TOP><INPUT TYPE="radio" NAME="payby" VALUE="$_"!;
+        if ($payby eq $_) {
+          $OUT .= qq! CHECKED> $paybychecked{$_}</TD>!;
+        } else {
+          $OUT .= qq!> $payby{$_}</TD>!;
+        }
+
+      }
+    }
+  %>
+
+</TR></TABLE><font color="#ff0000">*</font> required fields for each billing type
+<BR><BR>First package
+<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=0 WIDTH="100%">
+<TR>
+  <TD COLSPAN=2><SELECT NAME="pkgpart"><OPTION VALUE="">(none)
+
+  <%=
+    foreach my $package ( @{$packages} ) {
+      $OUT .= '<OPTION VALUE="'. $package->{'pkgpart'}. '"';
+      $OUT .= ' SELECTED' if $pkgpart && $package->{'pkgpart'} == $pkgpart;
+      $OUT .= '>'. $package->{'pkg'};
+    }
+  %>
+
+  </SELECT></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Username</TD>
+  <TD><INPUT TYPE="text" NAME="username" VALUE="<%= $username %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Password</TD>
+  <TD><INPUT TYPE="password" NAME="_password" VALUE="<%= $password %>"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Re-enter Password</TD>
+  <TD><INPUT TYPE="password" NAME="_password2" VALUE="<%= $password2 %>"></TD>
+</TR>
+<%=
+  if ( $init_data->{'security_phrase'} ) {
+    $OUT .= <<ENDOUT;
+<TR>
+  <TD ALIGN="right">Security Phrase</TD>
+  <TD><INPUT TYPE="text" NAME="sec_phrase" VALUE="$sec_phrase">
+  </TD>
+</TR>
+ENDOUT
+  } else {
+    $OUT .= '<INPUT TYPE="hidden" NAME="sec_phrase" VALUE="">';
+  }
+%>
+<%=
+  if ( scalar(@$pops) ) {
+    $OUT .= '<TR><TD ALIGN="right">Access number</TD><TD>'.
+            popselector($popnum). '</TD></TR>';
+  } else {
+    $OUT .= popselector($popnum);
+  }
+%>
+</TABLE>
+<BR><BR><INPUT TYPE="submit" VALUE="Signup">
+</FORM></BODY></HTML>
diff --git a/fs_signup/FS-SignupClient/cgi/stateselect.html b/fs_signup/FS-SignupClient/cgi/stateselect.html
new file mode 100644 (file)
index 0000000..39823be
--- /dev/null
@@ -0,0 +1,80 @@
+<HTML><HEAD><TITLE>ISP Signup</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>ISP Signup - state selection</FONT><BR><BR>
+<SCRIPT>
+function gotoURL(object) {
+    window.location.href = object.options[object.selectedIndex].value;
+}
+</SCRIPT>
+<FORM>
+Select your state: 
+<SELECT NAME="init_popstate" onChange="gotoURL(this.form.init_popstate)">
+<OPTION VALUE="stateselect.html"></OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AL">ALABAMA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AK">ALASKA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AS">AMERICAN SAMOA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AZ">ARIZONA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AR">ARKANSAS</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=CA">CALIFORNIA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=CO">COLORADO</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=CT">CONNECTICUT</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=DE">DELAWARE</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=DC">DISTRICT OF COLUMBIA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=FM">FEDERATED STATES OF MICRONESIA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=FL">FLORIDA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=GA">GEORGIA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=GU">GUAM</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=HI">HAWAII</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=ID">IDAHO</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=IL">ILLINOIS</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=IN">INDIANA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=IA">IOWA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=KS">KANSAS</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=KY">KENTUCKY</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=LA">LOUISIANA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=ME">MAINE</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=MH">MARSHALL ISLANDS</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=MD">MARYLAND</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=MA">MASSACHUSETTS</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=MI">MICHIGAN</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=MN">MINNESOTA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=MS">MISSISSIPPI</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=MO">MISSOURI</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=MT">MONTANA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=NE">NEBRASKA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=NV">NEVADA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=NH">NEW HAMPSHIRE</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=NJ">NEW JERSEY</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=NM">NEW MEXICO</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=NY">NEW YORK</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=NC">NORTH CAROLINA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=ND">NORTH DAKOTA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=MP">NORTHERN MARIANA ISLANDS</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=OH">OHIO</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=OK">OKLAHOMA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=OR">OREGON</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=PW">PALAU</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=PA">PENNSYLVANIA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=PR">PUERTO RICO</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=RI">RHODE ISLAND</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=SC">SOUTH CAROLINA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=SD">SOUTH DAKOTA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=TN">TENNESSEE</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=TX">TEXAS</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=UT">UTAH</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=VT">VERMONT</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=VI">VIRGIN ISLANDS</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=VA">VIRGINIA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=WA">WASHINGTON</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=WV">WEST VIRGINIA</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=WI">WISCONSIN</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=WY">WYOMING</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AE">Armed Forces Africa</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AA">Armed Forces Americas</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AE">Armed Forces Canada</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AE">Armed Forces Europe</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AE">Armed Forces Middle East</OPTION>
+<OPTION VALUE="signup.cgi?init_popstate=AP">Armed Forces Pacific</OPTION>
+</SELECT>
+</FORM>
+</BODY>
+</HTML>
diff --git a/fs_signup/FS-SignupClient/cgi/success.html b/fs_signup/FS-SignupClient/cgi/success.html
new file mode 100644 (file)
index 0000000..397cc6c
--- /dev/null
@@ -0,0 +1,11 @@
+<HTML><HEAD><TITLE>Signup successful</TITLE></HEAD>
+<BODY BGCOLOR="#e8e8e8"><FONT SIZE=7>Signup successful</FONT><BR><BR>
+Thanks for signing up!
+<BR><BR>
+Signup information for <%= $email_name %>:
+<BR><BR>
+Username: <%= $username %><BR>
+Password: <%= $password %><BR>
+Access number: (<%= $ac %>) / <%= $exch %> - <%= $local %><BR>
+Package: <%= $pkg %><BR>
+</BODY></HTML>
diff --git a/fs_signup/FS-SignupClient/fs_signupd b/fs_signup/FS-SignupClient/fs_signupd
new file mode 100755 (executable)
index 0000000..85bd68a
--- /dev/null
@@ -0,0 +1,86 @@
+#!/usr/bin/perl -Tw
+#
+# fs_signupd
+#
+# This is run REMOTELY over ssh by fs_signup_server.
+
+use strict;
+use Socket;
+use Storable qw(nstore_fd fd_retrieve);
+use IO::Handle;
+
+use vars qw( $Debug );
+
+$Debug = 1;
+
+my $fs_signupd_socket = "/usr/local/freeside/fs_signupd_socket";
+my $pid_file = "$fs_signupd_socket.pid";
+
+$ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
+$ENV{'SHELL'} = '/bin/sh';
+$ENV{'IFS'} = " \t\n";
+$ENV{'CDPATH'} = '';
+$ENV{'ENV'} = '';
+$ENV{'BASH_ENV'} = '';
+
+$|=1;
+
+warn "[fs_signupd] Reading init data...\n" if $Debug;
+my $init_data = fd_retrieve(\*STDIN);
+
+warn "[fs_signupd] Creating $fs_signupd_socket\n" if $Debug;
+my $uaddr = sockaddr_un($fs_signupd_socket);
+my $proto = getprotobyname('tcp');
+socket(Server,PF_UNIX,SOCK_STREAM,0) or die "socket: $!";
+unlink($fs_signupd_socket);
+bind(Server, $uaddr) or die "bind: $!";
+listen(Server,SOMAXCONN) or die "listen: $!";
+
+if ( -e $pid_file ) {
+  open(PIDFILE,"<$pid_file");
+  #chomp( my $old_pid = <PIDFILE> );
+  my $old_pid = <PIDFILE>;
+  close PIDFILE;
+  $old_pid =~ /^(\d+)$/;
+  kill 'TERM', $1;
+}
+open(PIDFILE,">$pid_file");
+print PIDFILE "$$\n";
+close PIDFILE;
+
+warn "[fs_signupd] Entering main loop...\n" if $Debug;
+my $paddr;
+for ( ; $paddr = accept(Client,Server); close Client) {
+
+  chop( my $command = <Client> );
+
+  if ( $command eq "signup_info" ) {
+
+    warn "[fs_signupd] sending signup info...\n" if $Debug; 
+    nstore_fd($init_data, \*Client) or die "can't send init data: $!";
+    Client->flush;
+
+  } elsif ( $command eq "new_customer" ) {
+
+    #inefficient...
+
+    warn "[fs_signupd] reading customer signup...\n" if $Debug;
+    my $signup_data = fd_retrieve(\*Client);
+
+    warn "[fs_signupd] sending customer data to remote server...\n" if $Debug;
+    nstore_fd($signup_data, \*STDOUT) or die "can't send signup data: $!";
+    STDOUT->flush;
+
+    warn "[fs_signupd] reading error from remote server...\n" if $Debug;
+    my $error = <STDIN>;
+
+    warn "[fs_signupd] sending error to local client...\n" if $Debug;
+    print Client $error;
+    Client->flush;
+
+  } else {
+    die "unexpected command from client: $command";
+  }
+
+}
+
diff --git a/fs_signup/FS-SignupClient/test.pl b/fs_signup/FS-SignupClient/test.pl
new file mode 100644 (file)
index 0000000..b613695
--- /dev/null
@@ -0,0 +1,20 @@
+# Before `make install' is performed this script should be runnable with
+# `make test'. After `make install' it should work as `perl test.pl'
+
+######################### We start with some black magic to print on failure.
+
+# Change 1..1 below to 1..last_test_to_print .
+# (It may become useful if the test is moved to ./t subdirectory.)
+
+BEGIN { $| = 1; print "1..1\n"; }
+END {print "not ok 1\n" unless $loaded;}
+#blah#use FS::SignupClient;
+$loaded = 1;
+print "ok 1\n";
+
+######################### End of black magic.
+
+# Insert your test code below (better if it prints "ok 13"
+# (correspondingly "not ok 13") depending on the success of chunk 13
+# of the test code):
+
diff --git a/fs_signup/cck.template b/fs_signup/cck.template
new file mode 100644 (file)
index 0000000..f1db554
--- /dev/null
@@ -0,0 +1,14 @@
+SITE_FILE      8chrfile
+SITE_NAME      YourISP
+LOGIN  { $username }
+PASSWORD       { $password }
+PHONE_NUM      +1({ $ac }){ $exch }-{ $loc }
+DNS_ADDR       10.0.0.1
+DNS_ADDR2      10.0.0.2
+NNTP_HOST      news.yourisp.com
+SMTP_HOST      mail.yourisp.com
+DOMAIN_NAME    yourisp.com
+POP_SERVER     { $username }@mail.yourisp.com
+POP_PASSWORD   { $password }
+HOME_URL       http://www.yourisp.com
+EMAIL_ADDR     { $username }@yourisp.com
diff --git a/fs_signup/fs_signup_server b/fs_signup/fs_signup_server
new file mode 100755 (executable)
index 0000000..d6eb4a8
--- /dev/null
@@ -0,0 +1,289 @@
+#!/usr/bin/perl -Tw
+#
+# fs_signup_server
+#
+
+use strict;
+use vars qw($pid);
+use IO::Handle;
+use Storable qw(nstore_fd fd_retrieve);
+use Tie::RefHash;
+use Net::SSH qw(sshopen2);
+use FS::UID qw(adminsuidsetup);
+use FS::Conf;
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_main_county;
+use FS::cust_main;
+use FS::cust_bill;
+use FS::cust_pkg;
+use FS::Msgcat qw(gettext);
+
+use vars qw( $opt $Debug );
+
+$Debug = 2;
+
+my $user = shift or die &usage;
+&adminsuidsetup( $user ); 
+
+my $conf = new FS::Conf;
+
+if ($conf->exists('signup_server-quiet')) {
+    $FS::cust_bill::quiet = 1;
+    $FS::cust_pkg::quiet = 1;
+}
+
+#my @payby = qw(CARD PREPAY);
+my @payby = $conf->config('signup_server-payby');
+my $smtpmachine = $conf->config('smtpmachine');
+
+my $machine = shift or die &usage;
+
+my $agentnum = shift or die &usage;
+my $agent = qsearchs( 'agent', { 'agentnum' => $agentnum } ) or die &usage;
+my $pkgpart_href = $agent->pkgpart_hashref;
+
+my $refnum = shift or die &usage;
+
+#causing trouble for some folks
+#$SIG{CHLD} = sub { wait() };
+
+$SIG{HUP} = \&killssh;
+$SIG{INT} = \&killssh;
+$SIG{QUIT} = \&killssh;
+$SIG{TERM} = \&killssh;
+$SIG{PIPE} = \&killssh;
+sub killssh { kill 'TERM', $pid if $pid; exit; };
+
+my($fs_signupd)="/usr/local/sbin/fs_signupd";
+
+while (1) {
+  my($reader,$writer)=(new IO::Handle, new IO::Handle);
+  #seems to be broken - calling ->flush explicitly# $writer->autoflush(1);
+  warn "[fs_signup_server] Connecting to $machine...\n" if $Debug;
+  $pid = sshopen2($machine,$reader,$writer,$fs_signupd);
+
+  my @pops = qsearch('svc_acct_pop',{} );
+  my $init_data = {
+
+    #'_protocol' => 'signup',
+    #'_version' => '0.1',
+    #'_packet' => 'init'
+  
+    'cust_main_county' =>
+      [ map { $_->hashref } qsearch('cust_main_county', {}) ],
+      
+    'part_pkg' =>
+      [
+        #map { $_->hashref }
+        map { { 'payby' => [ $_->payby ], %{$_->hashref} } }
+          grep { $_->svcpart('svc_acct') && $pkgpart_href->{ $_->pkgpart } }
+            qsearch( 'part_pkg', { 'disabled' => '' } )
+      ],
+
+    'agentnum2part_pkg' =>
+      {
+        map {
+          my $href = $_->pkgpart_hashref;
+          $_->agentnum =>
+            [
+              map { { 'payby' => [ $_->payby ], %{$_->hashref} } }
+                grep { $_->svcpart('svc_acct') && $href->{ $_->pkgpart } }
+                  qsearch( 'part_pkg', { 'disabled' => '' } )
+            ];
+        } qsearch('agent', {} )
+      },
+
+    'svc_acct_pop' => [ map { $_->hashref } @pops ],
+
+    'security_phrase' => $conf->exists('security_phrase'),
+
+    'payby' => [ $conf->config('signup_server-payby') ],
+
+    'msgcat' => { map { $_=>gettext($_) } qw(
+      passwords_dont_match invalid_card unknown_card_type not_a
+    ) },
+
+    'statedefault' => $conf->config('statedefault') || 'CA',
+
+    'countrydefault' => $conf->config('countrydefault') || 'US',
+
+  };
+
+  warn "[fs_signup_server] Sending init data...\n" if $Debug;
+  nstore_fd($init_data, $writer) or die "can't send init data: $!";
+  $writer->flush;
+
+  warn "[fs_signup_server] Entering main loop...\n" if $Debug;
+  while (1) {
+    warn "[fs_signup_server] Reading (waiting for) signup data...\n" if $Debug;
+    my $signup_data = fd_retrieve($reader);
+
+    if ( $Debug > 1 ) {
+      warn join('',
+        map { "  $_ => ". $signup_data->{$_}. "\n" } keys %$signup_data );
+    }
+
+    warn "[fs_signup_server] Processing signup...\n" if $Debug;
+
+    my $error = '';
+
+    #things that aren't necessary in base class, but are for signup server
+      #return "Passwords don't match"
+      #  if $hashref->{'_password'} ne $hashref->{'_password2'}
+    $error ||= gettext('empty_password') unless $signup_data->{'_password'};
+    $error ||= gettext('no_access_number_selected')
+      unless $signup_data->{'popnum'} || !scalar(@pops);
+
+    #shares some stuff with htdocs/edit/process/cust_main.cgi... take any
+    # common that are still here and library them.
+    my $cust_main = new FS::cust_main ( {
+      #'custnum'          => '',
+      'agentnum'         => $signup_data->{agentnum} || $agentnum,
+      'refnum'           => $refnum,
+
+      map { $_ => $signup_data->{$_} } qw(
+        last first ss company address1 address2 city county state zip country
+        daytime night fax payby payinfo paydate payname referral_custnum comments
+      ),
+
+    } );
+
+    $error ||= "Illegal payment type"
+      unless grep { $_ eq $signup_data->{'payby'} } @payby;
+
+    $cust_main->payinfo($cust_main->daytime)
+      if $cust_main->payby eq 'LECB' && ! $cust_main->payinfo;
+
+    my @invoicing_list = split( /\s*\,\s*/, $signup_data->{'invoicing_list'} );
+
+    $signup_data->{'pkgpart'} =~ /^(\d+)$/ or '' =~ /^()$/;
+    my $pkgpart = $1;
+
+    my $part_pkg =
+      qsearchs( 'part_pkg', { 'pkgpart' => $pkgpart } )
+        or $error ||= "WARNING: unknown pkgpart: $pkgpart";
+    my $svcpart = $part_pkg->svcpart('svc_acct') unless $error;
+
+    my $cust_pkg = new FS::cust_pkg ( {
+      #later#'custnum' => $custnum,
+      'pkgpart' => $signup_data->{'pkgpart'},
+    } );
+    $error ||= $cust_pkg->check;
+
+    my $svc_acct = new FS::svc_acct ( {
+      'svcpart'   => $svcpart,
+      map { $_ => $signup_data->{$_} }
+        qw( username _password sec_phrase popnum ),
+    } );
+
+    my $y = $svc_acct->setdefault; # arguably should be in new method
+    $error ||= $y unless ref($y);
+
+    $error ||= $svc_acct->check;
+
+    use Tie::RefHash;
+    tie my %hash, 'Tie::RefHash';
+    %hash = ( $cust_pkg => [ $svc_acct ] );
+    $error ||= $cust_main->insert( \%hash, \@invoicing_list ); #msgcat
+
+    if ( ! $error && $conf->exists('signup_server-realtime') ) {
+
+      warn "[fs_signup_server] Billing customer...\n" if $Debug;
+
+      my $bill_error = $cust_main->bill;
+      warn "[fs_signup_server] error billing new customer: $bill_error"
+        if $bill_error;
+
+      $cust_main->apply_payments;
+      $cust_main->apply_credits;
+
+      $bill_error = $cust_main->collect;
+      warn "[fs_signup_server] error collecting from new customer: $bill_error"
+        if $bill_error;
+
+      if ( $cust_main->balance > 0 ) {
+
+        #this makes sense.  credit is "un-doing" the invoice
+        $cust_main->credit( $cust_main->balance, 'signup server decline' );
+        $cust_main->apply_credits;
+
+        #should check list for errors...
+        #$cust_main->suspend;
+        $cust_main->cancel;
+
+        $error = '_decline';
+      }
+    }
+
+    warn "[fs_signup_server] Sending results...\n" if $Debug;
+    print $writer $error, "\n";
+
+    next if $error;
+
+    if ( $conf->config('signup_server-email') ) {
+      warn "[fs_signup_server] Sending email...\n" if $Debug;
+
+      #false laziness w/FS::cust_bill::send & FS::cust_pay::delete
+      use Mail::Header;
+      use Mail::Internet 1.44;
+      use Date::Format;
+      my $from = $conf->config('invoice_from'); #??? as good as any
+      $ENV{MAILADDRESS} = $from;
+      my $header = new Mail::Header ( [
+        "From: $from",
+        "To: ". $conf->config('signup_server-email'),
+        "Sender: $from",
+        "Reply-To: $from",
+        "Date: ". time2str("%a, %d %b %Y %X %z", time),
+        "Subject: FREESIDE NOTIFICATION: Signup Server",
+      ] );
+      my $body = [
+        "This is an automatic message from your Freeside installation\n",
+        "informing you a customer has signed up via the signup server:\n",
+        "\n",
+        'custnum     : '. $cust_main->custnum. "\n",
+        'Name        : '. $cust_main->last. ", ". $cust_main->first. "\n",
+        'Agent       : '. $cust_main->agent->agent. "\n",
+        'Package     : '. $part_pkg->pkg. ' - '. $part_pkg->comment. "\n",
+        'Signup Date : '. time2str('%C', time). "\n",
+        'Username    : '. $svc_acct->username. "\n",
+        #'Password    : '. # config file to turn this on if noment insists
+        'Day phone   : '. $cust_main->daytime. "\n",
+        'Night phone : '. $cust_main->night. "\n",
+        'Address     : '. $cust_main->address1. "\n",
+        ( $cust_main->address2
+            ? '              '. $cust_main->address2. "\n"
+            : ''                                           ),
+        '              '. $cust_main->city. ', '. $cust_main->state. '  '.
+                          $cust_main->zip. "\n",
+        ( $cust_main->country eq 'US'
+            ? ''
+            : '              '. $cust_main->country. "\n" ),
+        "\n",
+      ];
+      #if ( $cust_main->balance > 0 ) {
+      #  push @$body,
+      #    "This customer has an outstanding balance and has been suspended.\n";
+      #}
+      my $message = new Mail::Internet ( 'Header' => $header, 'Body' => $body );
+      $!=0;
+      $message->smtpsend( Host => $smtpmachine )
+        or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
+          or warn "[fs_signup_server] can't send email to ".
+                   $conf->config('signup_server-email').
+                   " via server $smtpmachine with SMTP: $!";
+      #end-of-send mail
+    }
+
+  }
+  close $writer;
+  close $reader;
+  warn "connection to $machine lost!  waiting 60 seconds...\n";
+  sleep 60;
+  warn "reconnecting...\n";
+}
+
+sub usage {
+  die "Usage:\n\n  fs_signup_server user machine agentnum refnum\n";
+}
+
diff --git a/fs_signup/ieak.template b/fs_signup/ieak.template
new file mode 100755 (executable)
index 0000000..52edaa9
--- /dev/null
@@ -0,0 +1,40 @@
+[Entry]
+Entry_Name = The Internet
+[Phone]
+Dial_As_Is=no
+Phone_Number = { $exch. $loc }
+Area_Code = { $ac }
+Country_Code = 1
+Country_Id = 1
+[Server]
+Type = PPP
+SW_Compress = Yes
+PW_Encrypt = Yes
+Negotiate_TCP/IP = Yes
+Disable_LCP = No
+[TCP/IP]
+Specify_IP_Address = No
+Specity_Server_Address = No
+IP_Header_Compress = Yes
+Gateway_On_Remote = Yes
+[User]
+Name = { $username }
+Password = { $password }
+Display_Password = Yes
+[Internet_Mail]
+Email_Name = { $email_name }
+Email_Address = { $username }\@domain.tld
+POP_Server = mail.domain.tld
+POP_Server_Port_Number = 110
+POP_Login_Name = { $username }
+POP_Login_Password = { $password }
+SMTP_Server = mail.domain.tld
+SMTP_Server_Port_Number = 25
+Install_Mail = 1
+[Internet_News]
+NNTP_Server = news.domain.tld
+NNTP_Server_Port_Number = 119
+Logon_Required = No
+Install_News = 1
+[Branding]
+Window_Title = The Internet
diff --git a/fs_webdemo/register.cgi b/fs_webdemo/register.cgi
new file mode 100755 (executable)
index 0000000..8255822
--- /dev/null
@@ -0,0 +1,136 @@
+#!/usr/bin/perl -Tw
+#
+# $Id: register.cgi,v 1.5 2000-03-03 18:22:42 ivan Exp $
+
+use strict;
+use vars qw(
+             $datasrc $user $pass $x
+             $cgi $username $email 
+             $dbh $sth
+             );
+             #$freeside_bin $freeside_test $freeside_conf
+             #@pw_set @saltset
+             #$user_pw $crypt_pw 
+             #$header $msg
+use CGI;
+use CGI::Carp qw(fatalsToBrowser);
+use DBI;
+#use Mail::Internet;
+#use Mail::Header;
+#use Date::Format;
+
+$ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
+$ENV{'SHELL'} = '/bin/sh';
+$ENV{'IFS'} = " \t\n";
+$ENV{'CDPATH'} = '';
+$ENV{'ENV'} = '';
+$ENV{'BASH_ENV'} = '';
+
+#$freeside_bin = '/home/freeside/bin/';
+#$freeside_test = '/home/freeside/test/';
+#$freeside_conf = '/usr/local/etc/freeside/';
+
+$datasrc = 'DBI:mysql:http_auth';
+$user = "freeside";
+$pass = "maelcolm";
+
+##my(@pw_set)= ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
+##my(@pw_set)= ( 'a'..'z', 'A'..'Z', '0'..'9' );
+#@pw_set = ( 'a'..'z', '0'..'9' );
+#@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+
+###
+
+$cgi = new CGI;
+
+$username = $cgi->param('username');
+$username =~ /^\s*([a-z][\w]{0,15})\s*$/i
+  or &idiot("Illegal username.  Please use 1-16 alphanumeric characters, and start your username with a letter.");
+$username = lc($1);
+
+$email = $cgi->param('email');
+$email =~ /^([\w\-\.\+]+\@[\w\-\.]+)$/
+  or &idiot("Illegal email address.");
+$email = $1;
+
+###
+
+#$user_pw = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
+#$crypt_pw = crypt($user_pw,$saltset[int(rand(64))].$saltset[int(rand(64))]);
+
+###
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+###
+
+$dbh = DBI->connect( $datasrc, $user, $pass, {
+       'AutoCommit' => 'true',
+} ) or die "DBI->connect error: $DBI::errstr\n";
+$x = $DBI::errstr; #silly; to avoid "used only once" warning
+
+$sth = $dbh->prepare("INSERT INTO mysql_auth VALUES (". join(", ",
+  $dbh->quote($username),
+#  $dbh->quote("X"),
+#  $dbh->quote($crypt_pw),
+  $dbh->quote($email),
+  $dbh->quote('freeside'),
+  $dbh->quote('unconfigured'),
+). ")" );
+
+$sth->execute or &idiot("Username in use: ". $sth->errstr);
+
+$dbh->disconnect or die $dbh->errstr;
+
+###
+
+$|=1;
+print $cgi->header;
+print <<END;
+<HTML>
+  <HEAD>
+    <TITLE>Freeside demo registration successful</TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#FFFFFF">
+  <table>
+    <tr><td>
+    <p align=center>
+      <img border=0 alt="Silicon Interactive Software Design" src="http://www.sisd.com/freeside/small-logo.gif">
+    </td><td>
+    <center><font color="#ff0000" size=7>freeside demo registration successful</font></center>
+    </td></tr>
+  </table>
+  <P>Your sample database has been setup.  Your password and the URL for the
+    Freeside demo have been emailed to you.
+  </BODY>
+</HTML>
+END
+
+###
+
+sub idiot {
+  my($error)=@_;
+  print $cgi->header, <<END;
+<HTML>
+  <HEAD>
+    <TITLE>Registration error</TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#FFFFFF">
+    <CENTER>
+    <H4>Registration error</H4>
+    </CENTER>
+    <P><B>$error</B>
+    <P>Hit the <I>Back</I> button in your web browser, correct this mistake,
+       and submit the form again.
+  </BODY>
+</HTML>
+END
+  
+  exit;
+}
diff --git a/fs_webdemo/register.html b/fs_webdemo/register.html
new file mode 100644 (file)
index 0000000..acf9cff
--- /dev/null
@@ -0,0 +1,33 @@
+<HTML>
+  <HEAD>
+    <TITLE>
+      Freeside - Billing and account administration software for ISPs
+    </TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#ffffff">
+  <table>
+    <tr><td>
+      <A HREF="http://www.sisd.com/">
+        <IMG BORDER=0 SRC="small-logo.gif" ALIGN=LEFT>
+      </A>
+    </td><td>
+      <center><font color="#ff0000" size=7 size=+4>freeside demo registration</font></center>
+    </td></tr>
+  </table>
+<P>You will need to choose a username for access to the Freeside web demo.
+
+<P><FONT SIZE=+1 COLOR="#ff0000">A password
+      and the URL for your demo will be emailed to you, so don't waste your
+      time with non-deliverable addresses.</FONT>
+We will <B>not</B> give your email address to any third party,
+      nor will we send you any unsolicited email (or in fact any email after the automatic registration).
+    <FORM ACTION="register.cgi" METHOD="POST">
+      <PRE>
+Freeside username: <INPUT TYPE="text" NAME="username" MAXLENGTH=16>
+
+Email address:     <INPUT TYPE="text" NAME="email">
+</PRE>
+<BR><INPUT TYPE="Submit" VALUE="Register">
+    </FORM>
+  </BODY>
+</HTML>
diff --git a/fs_webdemo/registerd b/fs_webdemo/registerd
new file mode 100755 (executable)
index 0000000..6314d0a
--- /dev/null
@@ -0,0 +1,192 @@
+#!/usr/bin/perl -w
+#
+# $Id: registerd,v 1.8 2000-03-03 12:27:54 ivan Exp $
+
+use strict;
+use vars qw(
+             $freeside_conf
+             $mysql_data
+             $datasrc $user $pass $x
+             $dbh $sth
+             @pw_set @saltset
+             $header $msg
+           );
+            # $freeside_bin $freeside_test 
+            # $cgi $username $name $email $user_pw $crypt_pw 
+#use CGI;
+#use CGI::Carp qw(fatalsToBrowser);
+use DBI;
+use Mail::Internet;
+use Mail::Header;
+use Date::Format;
+
+#$ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
+#$ENV{'SHELL'} = '/bin/sh';
+#$ENV{'IFS'} = " \t\n";
+#$ENV{'CDPATH'} = '';
+#$ENV{'ENV'} = '';
+#$ENV{'BASH_ENV'} = '';
+
+#$freeside_bin = '/home/freeside/bin/';
+#$freeside_test = '/home/freeside/test/';
+$freeside_conf = '/usr/local/etc/freeside/';
+
+$mysql_data = "/var/lib/mysql";
+
+$datasrc = 'DBI:mysql:http_auth';
+$user = "freeside";
+$pass = "maelcolm";
+
+#my(@pw_set)= ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
+#my(@pw_set)= ( 'a'..'z', 'A'..'Z', '0'..'9' );
+@pw_set = ( 'a'..'z', '0'..'9' );
+@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+
+#die "not running as system user freeside"
+#  unless $> == scalar(getpwnam('freeside'));
+die "not running as root user"
+  unless $> == 0;
+
+$dbh = DBI->connect( $datasrc, $user, $pass, {
+       'AutoCommit' => 'true',
+} ) or die "DBI->connect error: $DBI::errstr\n";
+$x = $DBI::errstr; #silly; to avoid "used only once" warning
+
+while ( 1 ) {
+
+  $SIG{HUP} = 'IGNORE';
+  $SIG{INT} = 'IGNORE';
+  $SIG{QUIT} = 'IGNORE';
+  $SIG{TERM} = 'IGNORE';
+  $SIG{TSTP} = 'IGNORE';
+  $SIG{PIPE} = 'IGNORE';
+
+  $sth = $dbh->prepare("LOCK TABLES mysql_auth WRITE");
+  $sth->execute or die $sth->errstr;
+
+  $sth = $dbh->prepare(
+    'SELECT * FROM mysql_auth WHERE status = "unconfigured"'
+  );
+  $sth->execute or die $sth->errstr;
+  my $pending = $sth->fetchall_arrayref( {} );
+
+  $sth = $dbh->prepare(
+    'UPDATE mysql_auth SET status = "locked" WHERE status = "unconfigured"'
+  );
+  $sth->execute or die $sth->errstr;
+
+  $sth = $dbh->prepare("UNLOCK TABLES");
+  $sth->execute or die $sth->errstr;
+
+  #
+
+  foreach my $row ( @{$pending} ) {
+
+    my $username = $row->{'username'};
+    my $email = $row->{'passwd'};
+
+    system("/usr/bin/mysqladmin --user=$user --password=$pass ".
+      "create demo_$username >/dev/null");
+
+    system "cp -p $mysql_data/demo_template/* $mysql_data/demo_$username";
+
+    mkdir "${freeside_conf}conf.DBI:mysql:demo_$username", 0755;    
+    system "cp -pr ${freeside_conf}conf.DBI:mysql:demo_template/* ".
+           "${freeside_conf}conf.DBI:mysql:demo_$username";
+
+    mkdir "${freeside_conf}counters.DBI:mysql:demo_$username", 0755;    
+    system "cp -p ${freeside_conf}counters.DBI:mysql:demo_template/* ".
+           "${freeside_conf}counters.DBI:mysql:demo_$username";
+    chown scalar(getpwnam('freeside')), scalar(getgrnam('freeside')),
+           "${freeside_conf}counters.DBI:mysql:demo_$username";
+
+    system "cp -p ${freeside_conf}dbdef.DBI:mysql:demo_template ".
+           "${freeside_conf}dbdef.DBI:mysql:demo_$username";
+
+    open(INVOICE_FROM, ">${freeside_conf}conf.DBI:mysql:demo_$username/invoice_from")
+      or die "Can\'t open ${freeside_conf}conf.DBI:mysql:demo_$username/invoice_from: $!";
+    print INVOICE_FROM "$email\n";
+    close INVOICE_FROM;
+
+    open(LPR, ">${freeside_conf}conf.DBI:mysql:demo_$username/lpr")
+      or die "Can\'t open ${freeside_conf}conf.DBI:mysql:demo_$username/lpr: $!";
+    print LPR "mail $email";
+    close LPR;
+
+    open(FROM, ">${freeside_conf}conf.DBI:mysql:demo_$username/registries/internic/from")
+      or die "Can\'t open ${freeside_conf}conf.DBI:mysql:demo_$username/registries/internic/from: $!";
+    print FROM "$email\n";
+    close FROM;
+
+    open(TO, ">${freeside_conf}conf.DBI:mysql:demo_$username/registries/internic/to")
+      or die "Can\'t open ${freeside_conf}conf.DBI:mysql:demo_$username/registries/internic/to: $!";
+    print TO "$email\n";
+    close TO;
+
+    open(SECRETS, ">${freeside_conf}secrets.demo_$username")
+      or die "Can\'t open ${freeside_conf}secrets.demo_$username: $!";
+    chown scalar(getpwnam('freeside')), scalar(getgrnam('freeside')),
+          "${freeside_conf}secrets.demo_$username";
+    chmod 0600, "${freeside_conf}secrets.demo_$username";
+    print SECRETS "DBI:mysql:demo_$username\nfreeside\nmaelcolm\n";
+    close SECRETS;
+
+    open(MAPSECRETS, ">>${freeside_conf}mapsecrets")
+      or die "Can\'t open ${freeside_conf}mapsecrets: $!";
+    print MAPSECRETS "$username secrets.demo_$username\n";
+    close MAPSECRETS;
+
+    my $user_pw = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
+    my $crypt_pw =
+      crypt($user_pw,$saltset[int(rand(64))].$saltset[int(rand(64))]);
+
+    $sth = $dbh->prepare(
+      qq(UPDATE mysql_auth SET passwd = "$crypt_pw", status = "done" WHERE username = "$username")
+    );
+    $sth->execute or die $sth->errstr;
+
+    $ENV{SMTPHOSTS} = "localhost";
+    $ENV{MAILADDRESS} = 'ivan-fsreg@sisd.com';
+    $ENV{TZ} = "PST8PDT";
+    $header = Mail::Header->new( [
+      'From: ivan-fsreg@sisd.com',
+      "To: $email",
+      'Bcc: ivan-fsreg_bcc@sisd.com',
+      'Sender: ivan-fsreg@sisd.com',
+      'Reply-To: ivan-fsreg@sisd.com',
+      #'Date: '. time2str("%a, %d %b %Y %X %z", time ),
+      'Date: '. time2str("%a, %d %b %Y %X ", time ). "-0800",
+      'Subject: Freeside demo information',
+    ] );
+    $msg = Mail::Internet->new(
+      'Header' => $header,
+      'Body' => [
+    "Hello,\n",
+    "\n",
+    "Your sample Freeside database has been setup.\n",
+    "\n",
+    "Point your web browswer at http://freeside.sisd.com/ and use the following\n",
+    "authentication information:\n",
+    "\n",
+    "Username: $username\n",
+    "Password: $user_pw\n",
+    "\n",
+    "-- \n",
+    "ivan\n",
+                ]
+    );
+    $msg->smtpsend or die "Can\'t send registration email!";
+
+  }
+
+  $SIG{HUP} = 'DEFAULT';
+  $SIG{INT} = 'DEFAULT';
+  $SIG{QUIT} = 'DEFAULT';
+  $SIG{TERM} = 'DEFAULT';
+  $SIG{TSTP} = 'DEFAULT';
+  $SIG{PIPE} = 'DEFAULT';
+
+  sleep 5;
+
+}
+
diff --git a/fs_webdemo/registerd.Pg b/fs_webdemo/registerd.Pg
new file mode 100755 (executable)
index 0000000..f166846
--- /dev/null
@@ -0,0 +1,221 @@
+#!/usr/bin/perl -w
+#
+# $Id: registerd.Pg,v 1.11 2001-10-24 15:29:30 ivan Exp $
+
+use strict;
+use vars qw(
+             $freeside_conf
+             $mysql_data
+             $datasrc $user $pass $x
+             $dbh $sth
+             @pw_set @saltset
+             $header $msg
+           );
+            # $freeside_bin $freeside_test 
+            # $cgi $username $name $email $user_pw $crypt_pw 
+#use CGI;
+#use CGI::Carp qw(fatalsToBrowser);
+use DBI;
+use Mail::Internet;
+use Mail::Header;
+use Date::Format;
+
+#$ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
+#$ENV{'SHELL'} = '/bin/sh';
+#$ENV{'IFS'} = " \t\n";
+#$ENV{'CDPATH'} = '';
+#$ENV{'ENV'} = '';
+#$ENV{'BASH_ENV'} = '';
+
+#$freeside_bin = '/home/freeside/bin/';
+#$freeside_test = '/home/freeside/test/';
+$freeside_conf = '/usr/local/etc/freeside/';
+
+#$mysql_data = "/var/lib/mysql";
+
+$datasrc = 'DBI:mysql:http_auth';
+$user = "freeside";
+$pass = "maelcolm";
+
+#my(@pw_set)= ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
+#my(@pw_set)= ( 'a'..'z', 'A'..'Z', '0'..'9' );
+@pw_set = ( 'a'..'z', '0'..'9' );
+@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+
+#die "not running as system user freeside"
+#  unless $> == scalar(getpwnam('freeside'));
+die "not running as root user"
+  unless $> == 0;
+
+$dbh = DBI->connect( $datasrc, $user, $pass, {
+       'AutoCommit' => 'true',
+} ) or die "DBI->connect error: $DBI::errstr\n";
+#$x = $DBI::errstr; #silly; to avoid "used only once" warning
+
+while ( 1 ) {
+
+  $SIG{HUP} = 'IGNORE';
+  $SIG{INT} = 'IGNORE';
+  $SIG{QUIT} = 'IGNORE';
+  $SIG{TERM} = 'IGNORE';
+  $SIG{TSTP} = 'IGNORE';
+  $SIG{PIPE} = 'IGNORE';
+
+  $sth = $dbh->prepare("LOCK TABLES mysql_auth WRITE");
+  $sth->execute or die $sth->errstr;
+
+  $sth = $dbh->prepare(
+    'SELECT * FROM mysql_auth WHERE status = "unconfigured"'
+  );
+  $sth->execute or die $sth->errstr;
+  my $pending = $sth->fetchall_arrayref( {} );
+
+  $sth = $dbh->prepare(
+    'UPDATE mysql_auth SET status = "locked" WHERE status = "unconfigured"'
+  );
+  $sth->execute or die $sth->errstr;
+
+  $sth = $dbh->prepare("UNLOCK TABLES");
+  $sth->execute or die $sth->errstr;
+
+  #
+
+  foreach my $row ( @{$pending} ) {
+
+    my $username = $row->{'username'};
+    my $email = $row->{'passwd'};
+
+    my $pdbh = DBI->connect( 'DBI:Pg:host=localhost;dbname=demo_template', 'freeside', 'maelcolm' )
+      or do { &myerr("$username: ". $DBI::errstr); next; };
+
+    my $psth = $pdbh->prepare("CREATE DATABASE demo_$username")
+      or do { &myerr("$username: ". $pdbh->errstr); next; };
+    $psth->execute()
+      or do { &myerr("$username: ". $psth->errstr); next; };
+
+    $pdbh->disconnect
+      or do { &myerr("fatal: $DBI::errstr"); die; };
+
+    open(PSQL,"|psql -U freeside demo_$username")
+      or do { &myerr("|psql -U freeside demo_$username: $!"); next; };
+    open(PSQLDATA, "</usr/local/etc/freeside/demo_template.Pg")
+      or do { &myerr("/usr/local/etc/freeside/demo_template.Pg: $!"); next; };
+    while(<PSQLDATA>) {
+      print PSQL $_;
+    }
+    close PSQLDATA
+      or do { &myerr("/usr/local/etc/freeside/demo_template.Pg: $!"); next; };
+    close PSQL
+      or do { &myerr("|psql -U freeside demo_$username: $!"); next; };
+
+    mkdir "${freeside_conf}conf.DBI:Pg:host=localhost;dbname=demo_$username", 0755;    
+    system "cp -pr ${freeside_conf}conf.DBI:Pg:host=localhost\\;dbname=demo_template/* ".
+           "${freeside_conf}conf.DBI:Pg:host=localhost\\;dbname=demo_$username";
+
+    mkdir "${freeside_conf}counters.DBI:Pg:host=localhost;dbname=demo_$username", 0755;    
+    system "cp -p ${freeside_conf}counters.DBI:Pg:host=localhost\\;dbname=demo_template/* ".
+           "${freeside_conf}counters.DBI:Pg:host=localhost\\;dbname=demo_$username";
+    chown scalar(getpwnam('freeside')), scalar(getgrnam('freeside')),
+           "${freeside_conf}counters.DBI:Pg:host=localhost;dbname=demo_$username";
+
+    system "cp -p ${freeside_conf}dbdef.DBI:Pg:host=localhost\\;dbname=demo_template ".
+           "${freeside_conf}dbdef.DBI:Pg:host=localhost\\;dbname=demo_$username";
+
+    open(INVOICE_FROM, ">${freeside_conf}conf.DBI:Pg:host=localhost;dbname=demo_$username/invoice_from")
+      or die "Can\'t open ${freeside_conf}conf.DBI:Pg:host=localhost;dbname=demo_$username/invoice_from: $!";
+    print INVOICE_FROM "$email\n";
+    close INVOICE_FROM;
+
+    open(LPR, ">${freeside_conf}conf.DBI:Pg:host=localhost;dbname=demo_$username/lpr")
+      or die "Can\'t open ${freeside_conf}conf.DBI:Pg:host=localhost;dbname=demo_$username/lpr: $!";
+    print LPR "mail $email";
+    close LPR;
+
+#    open(FROM, ">${freeside_conf}conf.DBI:Pg:host=localhost;dbname=demo_$username/registries/internic/from")
+#      or die "Can\'t open ${freeside_conf}conf.DBI:Pg:host=localhost;dbname=demo_$username/registries/internic/from: $!";
+#    print FROM "$email\n";
+#    close FROM;
+#
+#    open(TO, ">${freeside_conf}conf.DBI:Pg:host=localhost;dbname=demo_$username/registries/internic/to")
+#      or die "Can\'t open ${freeside_conf}conf.DBI:Pg:host=localhost;dbname=demo_$username/registries/internic/to: $!";
+#    print TO "$email\n";
+#    close TO;
+
+    open(SECRETS, ">${freeside_conf}secrets.demo_$username")
+      or die "Can\'t open ${freeside_conf}secrets.demo_$username: $!";
+    chown scalar(getpwnam('freeside')), scalar(getgrnam('freeside')),
+          "${freeside_conf}secrets.demo_$username";
+    chmod 0600, "${freeside_conf}secrets.demo_$username";
+    print SECRETS "DBI:Pg:host=localhost;dbname=demo_$username\nfreeside\nmaelcolm\n";
+    close SECRETS;
+
+    open(MAPSECRETS, ">>${freeside_conf}mapsecrets")
+      or die "Can\'t open ${freeside_conf}mapsecrets: $!";
+    print MAPSECRETS "$username secrets.demo_$username\n";
+    close MAPSECRETS;
+
+    my $user_pw = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) );
+    my $crypt_pw =
+      crypt($user_pw,$saltset[int(rand(64))].$saltset[int(rand(64))]);
+
+    $sth = $dbh->prepare(
+      qq(UPDATE mysql_auth SET passwd = "$crypt_pw", status = "done" WHERE username = "$username")
+    );
+    $sth->execute or die $sth->errstr;
+
+    #$ENV{SMTPHOSTS} = "localhost";
+    $ENV{SMTPHOSTS} = "192.168.1.1";
+    $ENV{MAILADDRESS} = 'ivan-fsreg@sisd.com';
+    $ENV{TZ} = "PST8PDT";
+    $header = Mail::Header->new( [
+      'From: ivan-fsreg@sisd.com',
+      "To: $email",
+      'Bcc: ivan-fsreg_bcc@sisd.com',
+      'Sender: ivan-fsreg@sisd.com',
+      'Reply-To: ivan-fsreg@sisd.com',
+      #'Date: '. time2str("%a, %d %b %Y %X %z", time ),
+      'Date: '. time2str("%a, %d %b %Y %X ", time ). "-0800",
+      'Subject: Freeside demo information',
+    ] );
+    $msg = Mail::Internet->new(
+      'Header' => $header,
+      'Body' => [
+    "Hello,\n",
+    "\n",
+    "Your sample Freeside database has been setup.\n",
+    "\n",
+    "Your login and database will be automatically deleted in 1-2 months.\n",
+    "\n",        
+    "Point your web browswer at http://freeside.sisd.com/ and use the following\n",
+    "authentication information:\n",
+    "\n",
+    "Username: $username\n",
+    "Password: $user_pw\n",
+    "\n",
+    "-- \n",
+    "ivan\n",
+                ]
+    );
+    $msg->smtpsend or die "Can\'t send registration email!";
+
+  }
+
+  $SIG{HUP} = 'DEFAULT';
+  $SIG{INT} = 'DEFAULT';
+  $SIG{QUIT} = 'DEFAULT';
+  $SIG{TERM} = 'DEFAULT';
+  $SIG{TSTP} = 'DEFAULT';
+  $SIG{PIPE} = 'DEFAULT';
+
+  sleep 5;
+
+}
+
+sub myerr {
+  my $msg = shift;
+  open(MAIL,"|mail ivan-fsdemoerr\@420.am");
+  print MAIL $msg, "\n\n";
+  print MAIL $msg, "\n\n";
+  close MAIL;
+};
+
diff --git a/htdocs/browse/agent.cgi b/htdocs/browse/agent.cgi
deleted file mode 100755 (executable)
index cf5f228..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# agent.cgi: browse agent
-#
-# ivan@sisd.com 97-dec-12
-#
-# changes to allow pages to load from a relative location in the web tree.
-#      bmccane@maxbaud.net     98-mar-25
-#
-# changed 'type' to 'atype' because type is reserved word in Pg6.3
-#      bmccane@maxbaud.net     98-apr-3
-#
-# agent type was linking to wrong cgi ivan@sisd.com 98-jul-18
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use FS::UID qw(cgisuidsetup swapuid);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-print header('Agent Listing', menubar(
-  'Main Menu' => '../',
-  'Add new agent' => '../edit/agent.cgi'
-)), <<END;
-    <BR>
-    Click on agent number to edit.
-    <TABLE BORDER>
-      <TR>
-        <TH><FONT SIZE=-1>Agent #</FONT></TH>
-        <TH>Agent</TH>
-        <TH>Type</TH>
-        <TH><FONT SIZE=-1>Freq. (unimp.)</FONT></TH>
-        <TH><FONT SIZE=-1>Prog. (unimp.)</FONT></TH>
-      </TR>
-END
-
-my($agent);
-foreach $agent ( sort { 
-  $a->getfield('agentnum') <=> $b->getfield('agentnum')
-} qsearch('agent',{}) ) {
-  my($hashref)=$agent->hashref;
-  my($typenum)=$hashref->{typenum};
-  my($agent_type)=qsearchs('agent_type',{'typenum'=>$typenum});
-  my($atype)=$agent_type->getfield('atype');
-  print <<END;
-      <TR>
-        <TD><A HREF="../edit/agent.cgi?$hashref->{agentnum}">
-          $hashref->{agentnum}</A></TD>
-        <TD>$hashref->{agent}</TD>
-        <TD><A HREF="../edit/agent_type.cgi?$typenum">$atype</A></TD>
-        <TD>$hashref->{freq}</TD>
-        <TD>$hashref->{prog}</TD>
-      </TR>
-END
-
-}
-
-print <<END;
-    </TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/browse/agent_type.cgi b/htdocs/browse/agent_type.cgi
deleted file mode 100755 (executable)
index 5f05bd5..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# agent_type.cgi: browse agent_type
-#
-# ivan@sisd.com 97-dec-10
-#
-# Changes to allow page to work at a relative position in server
-# Changes to make "Packages" display 2-wide in table (old way was too vertical)
-#      bmccane@maxbaud.net 98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use FS::UID qw(cgisuidsetup swapuid);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-print header("Agent Type Listing", menubar(
-  'Main Menu' => '../',
-  'Add new agent type' => "../edit/agent_type.cgi",
-)), <<END;
-    <BR>Click on agent type number to edit.
-    <TABLE BORDER>
-      <TR>
-        <TH><FONT SIZE=-1>Type #</FONT></TH>
-        <TH>Type</TH>
-        <TH colspan="2">Packages</TH>
-      </TR>
-END
-
-my($agent_type);
-foreach $agent_type ( sort { 
-  $a->getfield('typenum') <=> $b->getfield('typenum')
-} qsearch('agent_type',{}) ) {
-  my($hashref)=$agent_type->hashref;
-  my(@type_pkgs)=qsearch('type_pkgs',{'typenum'=> $hashref->{typenum} });
-  my($rowspan)=scalar(@type_pkgs);
-  $rowspan = int($rowspan/2+0.5) ;
-  print <<END;
-      <TR>
-        <TD ROWSPAN=$rowspan><A HREF="../edit/agent_type.cgi?$hashref->{typenum}">
-          $hashref->{typenum}
-        </A></TD>
-        <TD ROWSPAN=$rowspan>$hashref->{atype}</TD>
-END
-
-  my($type_pkgs);
-  my($tdcount) = -1 ;
-  foreach $type_pkgs ( @type_pkgs ) {
-    my($pkgpart)=$type_pkgs->getfield('pkgpart');
-    my($part_pkg) = qsearchs('part_pkg',{'pkgpart'=> $pkgpart });
-    print qq!<TR>! if ($tdcount == 0) ;
-    $tdcount = 0 if ($tdcount == -1) ;
-    print qq!<TD><A HREF="../edit/part_pkg.cgi?$pkgpart">!,
-          $part_pkg->getfield('pkg'),"</A></TD>";
-    $tdcount ++ ;
-    if ($tdcount == 2)
-    {
-       print qq!</TR>\n! ;
-       $tdcount = 0 ;
-    }
-  }
-
-  print "</TR>";
-}
-
-print <<END;
-    </TR></TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/browse/cust_main_county.cgi b/htdocs/browse/cust_main_county.cgi
deleted file mode 100755 (executable)
index d615198..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_main_county.cgi: browse cust_main_county
-#
-# ivan@sisd.com 97-dec-13
-#
-# Changes to allow page to work at a relative position in server
-#      bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use FS::UID qw(cgisuidsetup swapuid);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-print header("Tax Rate Listing", menubar(
-  'Main Menu' => '../',
-  'Edit tax rates' => "../edit/cust_main_county.cgi",
-)),<<END;
-    <BR>Click on <u>expand</u> to specify tax rates by county.
-    <P><TABLE BORDER>
-      <TR>
-        <TH><FONT SIZE=-1>State</FONT></TH>
-        <TH>County</TH>
-        <TH><FONT SIZE=-1>Tax</FONT></TH>
-      </TR>
-END
-
-my($cust_main_county);
-foreach $cust_main_county ( qsearch('cust_main_county',{}) ) {
-  my($hashref)=$cust_main_county->hashref;
-  print <<END;
-      <TR>
-        <TD>$hashref->{state}</TD>
-END
-
-  print "<TD>", $hashref->{county}
-      ? $hashref->{county}
-      : qq!(ALL) <FONT SIZE=-1>!.
-        qq!<A HREF="../edit/cust_main_county-expand.cgi?!. $hashref->{taxnum}.
-        qq!">expand</A></FONT>!
-    , "</TD>";
-
-  print <<END;
-        <TD>$hashref->{tax}%</TD>
-      </TR>
-END
-
-}
-
-print <<END;
-    </TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/browse/part_pkg.cgi b/htdocs/browse/part_pkg.cgi
deleted file mode 100755 (executable)
index e5ff31e..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# part_svc.cgi: browse part_pkg
-#
-# ivan@sisd.com 97-dec-5,9
-#
-# Changes to allow page to work at a relative position in server
-#      bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use FS::UID qw(cgisuidsetup swapuid);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-print header("Package Part Listing",menubar(
-  'Main Menu' => '../',
-  'Add new package' => "../edit/part_pkg.cgi",
-)), <<END;
-    <BR>Click on package part number to edit.
-    <TABLE BORDER>
-      <TR>
-        <TH><FONT SIZE=-1>Part #</FONT></TH>
-        <TH>Package</TH>
-        <TH>Comment</TH>
-        <TH><FONT SIZE=-1>Setup Fee</FONT></TH>
-        <TH><FONT SIZE=-1>Freq.</FONT></TH>
-        <TH><FONT SIZE=-1>Recur. Fee</FONT></TH>
-        <TH>Service</TH>
-        <TH><FONT SIZE=-1>Quan.</FONT></TH>
-      </TR>
-END
-
-my($part_pkg);
-foreach $part_pkg ( sort { 
-  $a->getfield('pkgpart') <=> $b->getfield('pkgpart')
-} qsearch('part_pkg',{}) ) {
-  my($hashref)=$part_pkg->hashref;
-  my(@pkg_svc)=grep $_->getfield('quantity'),
-    qsearch('pkg_svc',{'pkgpart'=> $hashref->{pkgpart} });
-  my($rowspan)=scalar(@pkg_svc);
-  print <<END;
-      <TR>
-        <TD ROWSPAN=$rowspan><A HREF="../edit/part_pkg.cgi?$hashref->{pkgpart}">
-          $hashref->{pkgpart}
-        </A></TD>
-        <TD ROWSPAN=$rowspan>$hashref->{pkg}</TD>
-        <TD ROWSPAN=$rowspan>$hashref->{comment}</TD>
-        <TD ROWSPAN=$rowspan>$hashref->{setup}</TD>
-        <TD ROWSPAN=$rowspan>$hashref->{freq}</TD>
-        <TD ROWSPAN=$rowspan>$hashref->{recur}</TD>
-END
-
-  my($pkg_svc);
-  foreach $pkg_svc ( @pkg_svc ) {
-    my($svcpart)=$pkg_svc->getfield('svcpart');
-    my($part_svc) = qsearchs('part_svc',{'svcpart'=> $svcpart });
-    print qq!<TD><A HREF="../edit/part_svc.cgi?$svcpart">!,
-          $part_svc->getfield('svc'),"</A></TD><TD>",
-          $pkg_svc->getfield('quantity'),"</TD></TR><TR>\n";
-  }
-
-  print "</TR>";
-}
-
-print <<END;
-    </TR></TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/browse/part_referral.cgi b/htdocs/browse/part_referral.cgi
deleted file mode 100755 (executable)
index b16fa89..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# part_referral.cgi: Browse part_referral
-#
-# ivan@sisd.com 98-feb-23 
-#
-# Changes to allow page to work at a relative position in server
-#      bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use FS::UID qw(cgisuidsetup swapuid);
-use FS::Record qw(qsearch);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-print header("Referral Listing", menubar(
-  'Main Menu' => '../',
-  'Add new referral' => "../edit/part_referral.cgi",
-)), <<END;
-    <BR>Click on referral number to edit.
-    <TABLE BORDER>
-      <TR>
-        <TH><FONT SIZE=-1>Referral #</FONT></TH>
-        <TH>Referral</TH>
-      </TR>
-END
-
-my($part_referral);
-foreach $part_referral ( sort { 
-  $a->getfield('refnum') <=> $b->getfield('refnum')
-} qsearch('part_referral',{}) ) {
-  my($hashref)=$part_referral->hashref;
-  print <<END;
-      <TR>
-        <TD><A HREF="../edit/part_referral.cgi?$hashref->{refnum}">
-          $hashref->{refnum}</A></TD>
-        <TD>$hashref->{referral}</TD>
-      </TR>
-END
-
-}
-
-print <<END;
-    </TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/browse/part_svc.cgi b/htdocs/browse/part_svc.cgi
deleted file mode 100755 (executable)
index 71a5564..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# part_svc.cgi: browse part_svc
-#
-# ivan@sisd.com 97-nov-14, 97-dec-9
-#
-# Changes to allow page to work at a relative position in server
-#      bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use FS::UID qw(cgisuidsetup swapuid);
-use FS::Record qw(qsearch);
-use FS::part_svc qw(fields);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-print header('Service Part Listing', menubar(
-  'Main Menu' => '../',
-  'Add new service' => "../edit/part_svc.cgi",
-)),<<END;
-    <BR>Click on service part number to edit.
-    <TABLE BORDER>
-      <TR>
-        <TH>Part #</TH>
-        <TH>Service</TH>
-        <TH>Table</TH>
-        <TH>Field</TH>
-        <TH>Action</TH>
-        <TH>Value</TH>
-      </TR>
-END
-
-my($part_svc);
-foreach $part_svc ( sort {
-  $a->getfield('svcpart') <=> $b->getfield('svcpart')
-} qsearch('part_svc',{}) ) {
-  my($hashref)=$part_svc->hashref;
-  my($svcdb)=$hashref->{svcdb};
-  my(@rows)=
-    grep $hashref->{${svcdb}.'__'.$_.'_flag'},
-      map { /^${svcdb}__(.*)$/; $1 }
-        grep ! /_flag$/,
-          grep /^${svcdb}__/,
-            fields('part_svc')
-  ;
-  my($rowspan)=scalar(@rows);
-  print <<END;
-      <TR>
-        <TD ROWSPAN=$rowspan><A HREF="../edit/part_svc.cgi?$hashref->{svcpart}">
-          $hashref->{svcpart}
-        </A></TD>
-        <TD ROWSPAN=$rowspan>$hashref->{svc}</TD>
-        <TD ROWSPAN=$rowspan>$hashref->{svcdb}</TD>
-END
-  my($row);
-  foreach $row ( @rows ) {
-    my($flag)=$part_svc->getfield($svcdb.'__'.$row.'_flag');
-    print "<TD>$row</TD><TD>";
-    if ( $flag eq "D" ) { print "Default"; }
-      elsif ( $flag eq "F" ) { print "Fixed"; }
-      else { print "(Unknown!)"; }
-    print "</TD><TD>",$part_svc->getfield($svcdb."__".$row),"</TD></TR><TR>";
-  }
-print "</TR>";
-}
-
-print <<END;
-    </TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/browse/svc_acct_pop.cgi b/htdocs/browse/svc_acct_pop.cgi
deleted file mode 100755 (executable)
index a8a3a92..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# svc_acct_pop.cgi: browse pops 
-#
-# ivan@sisd.com 98-mar-8
-#
-# Changes to allow page to work at a relative position in server
-#      bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use FS::UID qw(cgisuidsetup swapuid);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-print header('POP Listing', menubar(
-  'Main Menu' => '../',
-  'Add new POP' => "../edit/svc_acct_pop.cgi",
-)), <<END;
-    <BR>Click on pop number to edit.
-    <TABLE BORDER>
-      <TR>
-        <TH><FONT SIZE=-1>POP #</FONT></TH>
-        <TH>City</TH>
-        <TH>State</TH>
-        <TH>Area code</TH>
-        <TH>Exchange</TH>
-      </TR>
-END
-
-my($svc_acct_pop);
-foreach $svc_acct_pop ( sort { 
-  $a->getfield('popnum') <=> $b->getfield('popnum')
-} qsearch('svc_acct_pop',{}) ) {
-  my($hashref)=$svc_acct_pop->hashref;
-  print <<END;
-      <TR>
-        <TD><A HREF="../edit/svc_acct_pop.cgi?$hashref->{popnum}">
-          $hashref->{popnum}</A></TD>
-        <TD>$hashref->{city}</TD>
-        <TD>$hashref->{state}</TD>
-        <TD>$hashref->{ac}</TD>
-        <TD>$hashref->{exch}</TD>
-      </TR>
-END
-
-}
-
-print <<END;
-    </TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/docs/CGI-modules-2.76-patch.txt b/htdocs/docs/CGI-modules-2.76-patch.txt
deleted file mode 100755 (executable)
index 55b50bb..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-ivan@rootwood:~/src/CGI-modules-2.76/CGI$ diff -c Base.pm Base.pm.orig 
-*** Base.pm     Sat Jul 18 00:33:21 1998
---- Base.pm.orig        Sat Jul 18 00:06:12 1998
-***************
-*** 938,945 ****
-      my $orig_uri = $self->get_uri;
-      $self->log("Redirecting $CGI::Base::REQUEST_METHOD $orig_uri to $to_uri")
-        if $Debug;
-!     my $msg =   ($perm) ? StatusHdr(301,"Moved Permanently")
-!                       : StatusHdr(302,"Moved Temporarily");
-      my $hdrs = SendHeaders($msg, LocationHdr($to_uri));
-      $self->log($hdrs);
-  }
---- 938,945 ----
-      my $orig_uri = $self->get_uri;
-      $self->log("Redirecting $CGI::Base::REQUEST_METHOD $orig_uri to $to_uri")
-        if $Debug;
-!     my $msg =   ($perm) ? ServerHdr(301,"Moved Permanently")
-!                       : ServerHdr(302,"Moved Temporarily");
-      my $hdrs = SendHeaders($msg, LocationHdr($to_uri));
-      $self->log($hdrs);
-  }
-
diff --git a/htdocs/docs/admin.html b/htdocs/docs/admin.html
deleted file mode 100644 (file)
index 8adddbe..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<head>
-  <title>Administration</title>
-</head>
-<body>
-  <h1>Administration</h1>
-</body>
diff --git a/htdocs/docs/billing.html b/htdocs/docs/billing.html
deleted file mode 100644 (file)
index 02bfbd7..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-<head>
-  <title>Billing</title>
-</head>
-<body>
-  <h1>Billing</h1>
-  The bin/bill script can be run daily to bill all customers.  Usage: bill [ -c [ i ] ] [ -d <i>date</i> ] [ -b ]
-  <ul>
-    <li>-c: Turn on collecting (you probably want this).
-    <li>-i: Real-time billing (as opposed to bacth billing).  Only relevant for credit cards.  Not available without modifying site_perl/Bill.pm
-    <li>-d: Pretend it is <i>date</i> (parsed by Date::Parse)
-    <li>-b: N/A
-  </ul>
-  Printing should be configured on your freeside machine to print invoices.
-  <br><br>Batch credit card processing
-  <ul>
-    <li>After this script is run, a credit card batch will be in the <a href="schema.html#cust_pay_batch">cust_pay_batch</a> table.  Export this table to your credit card batching.
-    <li>When your batch completes, erase the cust_pay_batch records in that batch and add any necessary paymants to the <a href="schema.html#cust_pay">cust_pay</a> table.  Example code to add payments is:
-    <pre>use FS::cust_pay;
-
-# loop over all records in batch
-
-my $payment=create FS::cust_pay (
-  'invnum' => $invnum,
-  'paid' => $paid,
-  '_date' => $_date,
-  'payby' => $payby,
-  'payinfo' => $payinfo,
-  'paybatch' => $paybatch,
-);
-
-my $error=$payment->insert;
-if ( $error ) {
-  #process error
-}
-
-# end loop
-</pre>
-All fields except paybatch are contained in the cust_pay_batch table.  You can use paybatch field to track particular batches and/or particular transactions within a batch.
-  </ul>
-</body>
diff --git a/htdocs/docs/config.html b/htdocs/docs/config.html
deleted file mode 100644 (file)
index 9b80026..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<head>
-  <title>Configuration files</title>
-</head>
-<body>
-  <h1>Configuration files</h1>
-Configuration files and directories are located in `/var/spool/freeside/conf'.
-<ul>
-  <li>address - Your company name and address, four lines.
-  <li>bsdshellmachines - Your BSD flavored shell (and mail) machines, one per line.  This enables export of `/etc/passwd' and `/etc/master.passwd'.
-  <li>cybercash2 - <a href="http://www.cybercash.com/cybercash/services/cashreg214.html">CyberCash v2</a> support, four lines: paymentserverhost, paymentserverport, paymentserversecret, and transaction type (`mauthonly' or `mauthcapture').  CCLib.pm is required.
-  <li>cybercash3.2 - <a href="http://www.cybercash.com/cybercash/services/technology.html">CyberCash v3.2</a> support.  Two lines: the full path and name of your merchant_conf file, and the transaction type (`mauthonly' or `mauthcapture').  CCMckLib3_2.pm, CCMckDirectLib3_2.pm and CCMckErrno3_2 are required.
-  <li>domain - Your domain name.
-  <li>erpcdmachines - Your ERPCD authenticaion machines, one per line.  This enables export of `/usr/annex/acp_passwd' and `/usr/annex/acp_dialup'.
-  <li>home - For new users, prefixed to usrename to create a directory name.  Should have a leading but not a trailing slash.
-  <li>lpr - Print command for paper invoices, for example `lpr -h'.
-  <li>nismachines - Your NIS master (not slave master) machines, one per line.  This enables export of `/etc/global/passwd' and `/etc/global/shadow'.
-  <li>qmailmachines - Your qmail machines, one per line.  This enables export of `/var/qmail/control/virtualdomains', `/var/qmail/control/recipientmap', and `/var/qmail/control/rcpthosts'.  The existance of this file (even if empty) also turns on user `.qmail-extension' file maintenance in conjunction with `shellmachine'.
-  <li>radiusmachines - Your RADIUS authentication machines, one per line.  This enables export of `/etc/raddb/users'.
-  <li>registries - Directory which contains domain registry information.  Each registry is a directory.
-    <ul>
-      <li>registries/internic - Currently the only supported registry
-        <ul>
-          <li>registries/internic/from - Email address from which InterNIC domain registrations are sent.
-          <li>regestries/internic/nameservers - The nameservers for InterNIC domain registrations, one per line.  Each line contains an IP address and hostname, separated by whitespace.
-          <li>registries/internic/tech_contact - Technical contact NIC handle for domain registrations.
-          <li>registries/internic/template - Template for InterNIC domain registrations with special markup.  A suitable copy of the InterNIC domain template v4.0 is in `fs-x.y.z/etc/domain-template.txt'.
-          <li>registries/internic/to - Email address to which InterNIC domain registrations are sent.
-        </ul>
-    </ul>
-  <li>secrets - Three lines: Database engine datasource (for example, `DBI:mysql:freeside' or `DBI:Pg:dbname=freeside'), username, and password.  This file should not be world readable.
-  <li>sendmailmachines - Your sendmail machines, one per line.  This enables export of `/etc/virtusertable' and `/etc/sendmail.cw'.
-  <li>shellmachine - A single machine with user home directories mounted.  This enables home directory creation, renaming and archiving/deletion.  In conjunction with `qmailmachines', it also enables `.qmail-extension' file maintenance.
-  <li>shellmachines - Your Linux and System V flavored shell (and mail) machines, one per line.  This enables export of `/etc/passwd' and `/etc/shadow' files.
-  <li>shells - Legal shells (think /etc/shells).  You probably want to `cut -d: -f7 /etc/passwd | sort | uniq' initially so that importing doesn't fail with `Illegal shell' errors, then remove any special entries afterwords.  A blank line specifies that an empty shell is permitted.
-  <li>smtpmachine - SMTP relay for Freeside's outgoing mail.
-</ul>
-</body>
-
diff --git a/htdocs/docs/export.html b/htdocs/docs/export.html
deleted file mode 100644 (file)
index f760b97..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<head>
-  <title>File exporting</title>
-</head>
-<body>
-  <h1>File exporting</h1>
-  <ul>
-    <li>bin/svc_acct.export will create UNIX `passwd', `shadow' and `master.passwd' files, ERPCD `acp_passwd' and `acp_dialup' files and a RADIUS `users' file in the `/var/spool/freeside/export' directory.  Using the appropriate <a href="config.html">configuration files</a>, you can export these files to your remote machines unattended; see below.
-      <ul>
-        <li>shellmachines - passwd and shadow are copied to the remote machine as /etc/passwd.new and /etc/shadow.net and then moved to /etc/passwd and /etc/shadow if no errors occur.
-        <li>bsdshellmachines - passwd and master.passwd are copied to the remote machine as /etc/passwd.new and /etc/master.passwd.new and moved to /etc/passwd and /etc/master.passwd if no errors occur.
-        <li>nismachines - passwd and shadow are copied to the `/etc/global' directory on the remote machine.  If no errors occur, the command `( cd /var/yp; make; )' is executed on the remote machine.
-        <li>erpcdmachines - acp_passwd and acp_dialup are copied to the `/usr/annex' directory on the remote machine.  If no errors occur, the command `( kill -USR1 `cat /usr/annex/erpcd.pid` )' is executed on the remote machine. 
-        <li>radiusmachines - users is copied to the `/etc/raddb' directory on the remote machine.  If no errors occur, the command `( builddbm )' is executed on the remote machine.
-      </ul>
-    <li>site_perl/svc_acct.pm - If a shellmachine is defined, users can be created, modified and deleted remotely; see below.
-      <ul>
-        <li>The command `useradd -d <i>homedir</i> -s <i>shell</i> -u <i>uid</i> <i>username</i>' is executed when a user is added.
-        <li>The command `userdel <i>username</i>' is executed with a user is deleted.
-        <li>If a user's home directory changes, the command `[ -d <i>old_homedir</i> &amp;&amp; ( chmod u+t <i>old_homedir</i>; umask 022; mkdir <i>new_homedir</i>; cd <i>old_homedir</i>; find . -depth -print | cpio -pdm <i>new_homedir</i>; chmod u-t <i>new_homedir</i>; chown -R <i>uid</i>.<i>gid</i> <i>new_homedir</i>; rm -rf <i>old_homedir</i> )' is executed.
-      </ul>
-    <li>bin/svc_acct_sm.export will create <a href="http://www.qmail.org">Qmail</a> `rcpthosts', `recipientmap' and `virtualdomains' files and <a href="http://www.sendmail.org">Sendmail</a> `virtusertable' and `sendmail.cw' files in the `/var/spool/freeside/export' directory.  Using the appropriate <a href="config.html">configuration files</a>, you can export these files to your remote machines unattemded; see below.
-      <ul>
-        <li>qmailmachines - recipientmap, virtualdomains and rcpthosts are copied to the `/var/qmail/control' directory on the remote machine.  Note: If you <a href="legacy.html#svc_acct_sm">imported</a> qmail configuration files, run the generated `/var/spool/freeside/export/virtualdomains.FIX' on a machine with your user home directories before exporting qmail configuration files.
-        <li>shellmachine - The command `[ -e <i>homedir</i>/.qmail-default ] || { touch <i>homedir</i>/.qmail-default; chown <i>uid</i>.<i>gid</i> <i>homedir</i>/.qmail-default; }' will be run on this machine for users in the virtualdomains file.
-        <li>sendmailmachines - sendmail.cw and virtusertable are copied to the remote machine as /etc/sendmail.cw.new and /etc/virtusertable.new and moved to /etc/sendmail.cw and /etc/virtusertable if no errors occur.
-      </ul>
-    <li>site_perl/svc_acct_sm.pm - If the qmailmachines configuration file exists and a shellmachine is defined, user `.qmail-' files can be updated.
-      <ul>
-        <li>The command `[ -e <i>homedir</i>/.qmail-<i>domain</i>-default ] || { touch <i>homedir</i>/.qmail-<i>domain</i>-default; chown <i>uid</i>.<i>gid</i> <i>homedir</i>/.qmail-<i>domain</i>-default; }' is run.
-      </ul>
-  </ul>
-  <br><a name=ssh>Unattended remote login</a> - Freeside can login to remote machines unattended using SSH.  This can pose a security risk if not configured correctly, and will allow an intruder who breaks into your freeside machine full access to your remote machines.  <b>Do not use this feature unless you understand what you are doing!</b>
-    <ul>
-      <li>As the freeside user (on your freeside machine), generate an authentication key using <a href="http://www.tac.nyc.ny.us/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>.  Since this is for unattended operation, you need to use a blank passphrase.
-      <li>Append the newly-created identity.pub file to root's authorized_keys on the remote machine(s).
-    </ul>
-
-</body>
-
diff --git a/htdocs/docs/index.html b/htdocs/docs/index.html
deleted file mode 100644 (file)
index 20051ca..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<head>
-  <title>Documentation</title>
-</head>
-<body>
-  <h1>Documentation</h1>
-<ul>
-  <li><a href="install.html">New Installation</a>
-  <li><a href="upgrade.html">Upgrading from 1.0.x to 1.1.x</a>
-  <li><a href="upgrade2.html">Upgrading from 1.1.x to 1.1.3</a>
-  <li><a href="config.html">Configuration files</a>
-<!--
-  <li><a href="admin.html">Administration</a>
-!-->
-  <li><a href="../index.html#admin">Administration</a>
-  <li><a href="legacy.html">Importing legacy data</a>
-  <li><a href="export.html">File exporting and remote setup</a>
-  <li><a href="passwd.html">fs_passwd</a>
-  <li><a href="billing.html">Billing</a>
-  <li><a href="trouble.html">Troubleshooting</a>
-  <li><a href="schema.html">Schema reference</a>
-  <li><a href="man/">Perl API</a>
-</ul>
-</body>
diff --git a/htdocs/docs/install.html b/htdocs/docs/install.html
deleted file mode 100644 (file)
index c4784eb..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-<head>
-  <title>Installation</title>
-</head>
-<body>
-<h1>Installation</h1>
-Before installing, you need:
-<ul>
-  <li>A web server, such as <a href="http://www.apache-ssl.org">Apache-SSL</a> or <a href="http://www.apache.org">Apache</a>
-  <li><a href="ftp://ftp.cs.hut.fi/pub/ssh/">SSH</a>
-  <li>agrep from the <a href="http://glimpse.cs.arizona.edu">Glimpse</a> distribution, if you want fuzzy searching capability
-  <li><a href="http://www.perl.com/CPANl/doc/relinfo/INSTALL.html">Perl</a> (at least 5.004_04)
-  <li>A database engine supported by Perl's <a href="http://www.hermetica.com/technologia/DBI/">DBI</a>, such as <a href="http://www.tcx.se/">MySQL</a> or <a href="http://www.postgresql.org/">PostgreSQL</a>
-  <li>Perl modules
-    <ul>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/MIME/">MIME-Base64</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/Data">Data-Dumper</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/MD5">MD5</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/Net">libnet</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/LWP/">libwww-perl</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/CGI/">CGI-modules</a> (<b>NOT</b> CGI.pm) with this <a href="CGI-modules-2.76-patch.txt">patch</a> applied
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/Business/">Business-CreditCard</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/Data/">Data-ShowTable</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/Mail/">MailTools</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/Time/">TimeDate</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/Date/">DateManip</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/File/">File-CounterFile</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/FreezeThaw/">FreezeThaw</a>
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/DBI/">DBI
-      <li><a href="http://www.perl.com/CPAN/modules/by-module/DBD/">DBD for your database engine</a>
-    </ul>
-</ul>
-Install the Freeside distribution:
-<ul>
-  <li>Add the user `freeside' to your system.
-  <li>Add the freeside database to your database engine.  (with <a href="http://www.mysql.com/Manual_chapter/manual_Syntax.html#Create database">MySQL</a>) (with <a href="http://www.postgresql.org/docs/admin/manage-ag.htm#AEN854">PostgreSQL</a>)
-  <li>Allow the freeside user full access to the freeside database.  (with <a href="http://www.mysql.com/Manual_chapter/manual_Privilege_system.html#Privilege system">MySQL</a>) (with <a href="http://www.postgresql.org/docs/admin/newuser.htm">PostgreSQL</a>)
-  <li>Unpack the tarball: <pre>gunzip -c fs-x.y.z.tar.gz | tar xvf -</pre>
-  <li>Copy or link fs-x.y.z/site_perl to FS in your site_perl directory.  (try `<code>perl -V</code>' if unsure) <pre>mkdir /usr/local/lib/site_perl/FS
-cp fs-x.y.z/site_perl/* /usr/local/lib/site_perl/FS</pre> or <pre>ln -s /full/path/to/fs-x.y.z/site_perl /usr/local/lib/site_perl/FS</pre>
-  <li>Copy or link fs-x.y.z/htdocs to your web server's document space.  <pre>mkdir /usr/local/apache/htdocs/freeside
-cp -r fs-x.y.z/htdocs/* /usr/local/apache/htdocs/freeside</pre> or <pre>ln -s /full/path/to/fs-x.y.z/htdocs /usr/local/apache/htdocs/freeside</pre>
-  <li>Restrict access to this web interface.  (with <a href="http://www.apache.org/docs/misc/FAQ.html#user-authentication">Apache</a>)
-  <li>Enable CGI execution for files with the `.cgi' extension.  (with <a href="http://www.apache.org/docs/mod/mod_mime.html#addhandler">Apache</a>)
-  <li>Set ownership and permissions for the web interface.  Your system should support secure setuid scripts or Perl's emulation, see <a href="http://www.perl.com/CPAN-local/doc/manual/html/pod/perlsec.html#Security_Bugs">perlsec: Security Bugs</a> for information and workarounds.
-<pre>cd /usr/local/apache/htdocs/freeside
-chown -R freeside .
-chmod 4755 browse/*.cgi edit/*.cgi edit/process/*.cgi misc/*.cgi misc/process/*.cgi search/*.cgi view/*.cgi</pre>
-<li>Create the base Freeside directory `/var/spool/freeside', and the subdirectories `conf', `counters', and `export'.  <pre>mkdir /var/spool/freeside
-mkdir /var/spool/freeside/conf
-mkdir /var/spool/freeside/counters
-mkdir /var/spool/freeside/export
-chown -R freeside /var/spool/freeside</pre>
-  <li>Create the necessary <a href="config.html">configuration files</a>.
-  <li>Run bin/fs-setup to create the database tables.
-</ul>
-</body>
diff --git a/htdocs/docs/legacy.html b/htdocs/docs/legacy.html
deleted file mode 100644 (file)
index 40e09cb..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-<head>
-  <title>Importing legacy data</title>
-</head>
-<body>
-  <h1>Importing legacy data</h1>
-<ul>
-  <li><a name="svc_acct">bin/svc_acct.import</a> - Import `passwd', ( `shadow' or `master.passwd' ) and RADIUS `users'.  Before running bin/svc_acct.import, you need <a href="http://rootwood.sisd.com/freeside/browse/part_svc.cgi">services</a> (with table svc_acct) as follows:
-    <ul>
-      <li>Most accounts probably have entries in passwd and users (with Port-Limit nonexistant or 1)
-      <li>Some accounts have entries in passwd and users, but with Port-Limit 2 (or more)
-      <li>Some accounts might have entries in users only (Port-Limit 1)
-      <li>Some accounts might have entries in users only (Port-Limit >= 2)
-      <li>POP mail accounts have entries in passwd only, and have a particular shell.
-      <li>Everything else in passwd is a shell account.
-    </ul>
-  <li><a name="svc_acct_sm">bin/svc_acct_sm.import</a> - Import qmail ( `virtualdomains' and `rcpthosts' ), or sendmail ( `virtusertable' and `sendmail.cw' ) files.  Before running bin/svc_acct_sm.import, you need <a href="http://rootwood.sisd.com/freeside/browse/part_svc.cgi">services</a> as follows:
-    <ul>
-      <li>Domain (table svc_acct)
-      <li>Mail alias (table svc_acct_sm)
-    </ul>
-  <li><a name="cust_main">Importing customer data</a>
-    <ul>
-      <li>Manually
-        <ul>
-          <li>Add a <a href="../edit/cust_main.cgi">new customer</a>
-          <li>Add one or more packages for this customer
-          <li>Enter a package by clicking on the package number
-          <li>Pick the `Link to existing' option
-        </ul>
-      <li>Batch - You will need to write a script to import your particular legacy data.  You can use eg/TEMPLATE_cust_main.import as a starting point.
-    </ul>
-</ul>
-</body>
-
diff --git a/htdocs/docs/man/Bill.txt b/htdocs/docs/man/Bill.txt
deleted file mode 100644 (file)
index 545dd1a..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-NAME
-    FS::Bill - Legacy stub
-
-SYNOPSIS
-    The functionality of FS::Bill has been integrated into
-    FS::cust_main.
-
-HISTORY
-    ivan@voicenet.com 97-jul-24 - 25 - 28
-
-    use Safe; evaluate all fees with perl (still on TODO list until
-    I write some examples & test opmask to see if we can read db)
-    %hash=$obj->hash later ivan@sisd.com 98-mar-13
-
-    packages with no next bill date start at $time not time, this
-    should eliminate the last of the problems with billing at a past
-    date also rewrite the invoice priting logic not to print
-    invoices for things that haven't happended yet and update
-    $cust_bill->printed when we print so PAST DUE notices work, and
-    s/date/_date/ ivan@sisd.com 98-jun-4
-
-    more logic for past due stuff - packages with no next bill date
-    start at $cust_pkg->setup || $time ivan@sisd.com 98-jul-13
-
-    moved a few things in collection logic; negative charges should
-    work ivan@sisd.com 98-aug-6
-
-    pod, moved everything to FS::cust_main ivan@sisd.com 98-sep-19
-
diff --git a/htdocs/docs/man/CGI.txt b/htdocs/docs/man/CGI.txt
deleted file mode 100644 (file)
index 54f9b8a..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-NAME
-    FS::CGI - Subroutines for the web interface
-
-SYNOPSIS
-      use FS::CGI qw(header menubar idiot eidiot);
-
-      print header( 'Title', '' );
-      print header( 'Title', menubar('item', 'URL', ... ) );
-
-      idiot "error message"; 
-      eidiot "error message";
-
-DESCRIPTION
-    Provides a few common subroutines for the web interface.
-
-SUBROUTINES
-    header TITLE, MENUBAR
-        Returns an HTML header.
-
-    menubar ITEM, URL, ...
-        Returns an HTML menubar.
-
-    idiot ERROR
-        Sends headers and an HTML error message.
-
-    eidiot ERROR
-        Sends headers and an HTML error message, then exits.
-
-BUGS
-    Not OO.
-
-    Not complete.
-
-    Uses CGI-modules instead of CGI.pm
-
-SEE ALSO
-    the CGI::Base manpage
-
-HISTORY
-    subroutines for the HTML/CGI GUI, not properly OO. :(
-
-    ivan@sisd.com 98-apr-16 ivan@sisd.com 98-jun-22
-
-    lose the background, eidiot ivan@sisd.com 98-sep-2
-
-    pod ivan@sisd.com 98-sep-12
-
diff --git a/htdocs/docs/man/Conf.txt b/htdocs/docs/man/Conf.txt
deleted file mode 100644 (file)
index c46c9ee..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-NAME
-    FS::Conf - Read access to Freeside configuration values
-
-SYNOPSIS
-      use FS::Conf;
-
-      $conf = new FS::Conf;
-      $conf = new FS::Conf "/non/standard/config/directory";
-
-      $dir = $conf->dir;
-
-      $value = $conf->config('key');
-      @list  = $conf->config('key');
-      $bool  = $conf->exists('key');
-
-DESCRIPTION
-    Read access to Freeside configuration values. Keys currently map
-    to filenames, but this may change in the future.
-
-METHODS
-    new [ DIRECTORY ]
-        Create a new configuration object. Optionally, a non-default
-        directory may be specified.
-
-    dir Returns the directory.
-
-    config
-        Returns the configuration value or values (depending on
-        context) for key.
-
-    exists
-        Returns true if the specified key exists, even if the
-        corresponding value is undefined.
-
-BUGS
-    The option to specify a non-default directory should probably be
-    removed.
-
-    Write access (with locking) should be implemented.
-
-SEE ALSO
-    config.html from the base documentation contains a list of
-    configuration files.
-
-HISTORY
-    Ivan Kohler <ivan@sisd.com> 98-sep-6
-
diff --git a/htdocs/docs/man/Invoice.txt b/htdocs/docs/man/Invoice.txt
deleted file mode 100644 (file)
index 17953d5..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-NAME
-    FS::Invoice - Legacy stub
-
-SYNOPSIS
-    The functioanlity of FS::invoice has been integrated in
-    FS::cust_bill.
-
-HISTORY
-    ivan@voicenet.com 97-jun-25 - 27
-
-    maybe should be changed to be OO-functions on $cust_bill
-    objects? (instead of passing invnum, ugh).
-
-    ISA cust_bill and return inovice instead of passing filehandle
-    ivan@sisd.com 98-mar-13 (add postscript output!)
-
-    close our kid when we're done ivan@sisd.com 98-jun-4
-
-    separated code which shuffled data from code which formatted.
-    (so i could) fixed past due notices showing up when balance due
-    =< 0 return address comes from /var/spool/freeside/conf/address
-    ivan@sisd.com 98-jul-2
-
diff --git a/htdocs/docs/man/Record.txt b/htdocs/docs/man/Record.txt
deleted file mode 100644 (file)
index 0accb65..0000000
+++ /dev/null
@@ -1,332 +0,0 @@
-NAME
-    FS::Record - Database record objects
-
-SYNOPSIS
-        use FS::Record;
-        use FS::Record qw(dbh fields hfields qsearch qsearchs dbdef);
-
-        $record = new FS::Record 'table', \%hash;
-        $record = new FS::Record 'table', { 'column' => 'value', ... };
-
-        $record  = qsearchs FS::Record 'table', \%hash;
-        $record  = qsearchs FS::Record 'table', { 'column' => 'value', ... };
-        @records = qsearch  FS::Record 'table', \%hash; 
-        @records = qsearch  FS::Record 'table', { 'column' => 'value', ... };
-
-        $table = $record->table;
-        $dbdef_table = $record->dbdef_table;
-
-        $value = $record->get('column');
-        $value = $record->getfield('column');
-        $value = $record->column;
-
-        $record->set( 'column' => 'value' );
-        $record->setfield( 'column' => 'value' );
-        $record->column('value');
-
-        %hash = $record->hash;
-
-        $hashref = $record->hashref;
-
-        $error = $record->add;
-
-        $error = $record->del;
-
-        $error = $new_record->rep($old_record);
-
-        $value = $record->unique('column');
-
-        $value = $record->ut_float('column');
-        $value = $record->ut_number('column');
-        $value = $record->ut_numbern('column');
-        $value = $record->ut_money('column');
-        $value = $record->ut_text('column');
-        $value = $record->ut_textn('column');
-        $value = $record->ut_alpha('column');
-        $value = $record->ut_alphan('column');
-        $value = $record->ut_phonen('column');
-        $value = $record->ut_anythingn('column');
-
-        $dbdef = reload_dbdef;
-        $dbdef = reload_dbdef "/non/standard/filename";
-        $dbdef = dbdef;
-
-        $quoted_value = _quote($value,'table','field');
-
-        #depriciated
-        $fields = hfields('table');
-        if ( $fields->{Field} ) { # etc.
-
-        @fields = fields 'table';
-
-DESCRIPTION
-    (Mostly) object-oriented interface to database records. Records
-    are currently implemented on top of DBI. FS::Record is intended
-    as a base class for table-specific classes to inherit from, i.e.
-    FS::cust_main.
-
-METHODS
-    new TABLE, HASHREF
-        Creates a new record. It doesn't store it in the database,
-        though. See the section on "add" for that.
-
-        Note that the object stores this hash reference, not a
-        distinct copy of the hash it points to. You can ask the
-        object for a copy with the *hash* method.
-
-    qsearch TABLE, HASHREF
-        Searches the database for all records matching (at least)
-        the key/value pairs in HASHREF. Returns all the records
-        found as FS::Record objects.
-
-    qsearchs TABLE, HASHREF
-        Searches the database for a record matching (at least) the
-        key/value pairs in HASHREF, and returns the record found as
-        an FS::Record object. If more than one record matches, it
-        carps but returns the first. If this happens, you either
-        made a logic error in asking for a single item, or your data
-        is corrupted.
-
-    table
-        Returns the table name.
-
-    dbdef_table
-        Returns the FS::dbdef_table object for the table.
-
-    get, getfield COLUMN
-        Returns the value of the column/field/key COLUMN.
-
-    set, setfield COLUMN, VALUE
-        Sets the value of the column/field/key COLUMN to VALUE.
-        Returns VALUE.
-
-    AUTLOADED METHODS
-        $record->column is a synonym for $record->get('column');
-
-        $record->column('value') is a synonym for $record-
-        >set('column','value');
-
-    hash
-        Returns a list of the column/value pairs, usually for
-        assigning to a new hash.
-
-        To make a distinct duplicate of an FS::Record object, you
-        can do:
-
-            $new = new FS::Record ( $old->table, { $old->hash } );
-
-    hashref
-        Returns a reference to the column/value hash.
-
-    add Adds this record to the database. If there is an error, returns
-        the error, otherwise returns false.
-
-    del Delete this record from the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    rep OLD_RECORD
-        Replace the OLD_RECORD with this one in the database. If
-        there is an error, returns the error, otherwise returns
-        false.
-
-    unique COLUMN
-        Replaces COLUMN in record with a unique number. Called by
-        the add method on primary keys and single-field unique
-        columns (see the FS::dbdef_table manpage). Returns the new
-        value.
-
-    ut_float COLUMN
-        Check/untaint floating point numeric data: 1.1, 1, 1.1e10,
-        1e10. May not be null. If there is an error, returns the
-        error, otherwise returns false.
-
-    ut_number COLUMN
-        Check/untaint simple numeric data (whole numbers). May not
-        be null. If there is an error, returns the error, otherwise
-        returns false.
-
-    ut_numbern COLUMN
-        Check/untaint simple numeric data (whole numbers). May be
-        null. If there is an error, returns the error, otherwise
-        returns false.
-
-    ut_money COLUMN
-        Check/untaint monetary numbers. May be negative. Set to 0 if
-        null. If there is an error, returns the error, otherwise
-        returns false.
-
-    ut_text COLUMN
-        Check/untaint text. Alphanumerics, spaces, and the following
-        punctuation symbols are currently permitted: ! @ # $ % & ( )
-        - + ; : ' " , . ? / May not be null. If there is an error,
-        returns the error, otherwise returns false.
-
-    ut_textn COLUMN
-        Check/untaint text. Alphanumerics, spaces, and the following
-        punctuation symbols are currently permitted: ! @ # $ % & ( )
-        - + ; : ' " , . ? / May be null. If there is an error,
-        returns the error, otherwise returns false.
-
-    ut_alpha COLUMN
-        Check/untaint alphanumeric strings (no spaces). May not be
-        null. If there is an error, returns the error, otherwise
-        returns false.
-
-    ut_alpha COLUMN
-        Check/untaint alphanumeric strings (no spaces). May be null.
-        If there is an error, returns the error, otherwise returns
-        false.
-
-    ut_phonen COLUMN
-        Check/untaint phone numbers. May be null. If there is an
-        error, returns the error, otherwise returns false.
-
-    ut_anything COLUMN
-        Untaints arbitrary data. Be careful.
-
-SUBROUTINES
-    reload_dbdef([FILENAME])
-            Load a database definition (see the FS::dbdef manpage),
-            optionally from a non-default filename. This command is
-            executed at startup unless *$FS::Record::setup_hack* is
-            true. Returns a FS::dbdef object.
-
-    dbdef   Returns the current database definition. See the FS::dbdef
-            manpage.
-
-    _quote VALUE, TABLE, COLUMN
-            This is an internal function used to construct SQL
-            statements. It returns VALUE DBI-quoted (see the section
-            on "quote" in the DBI manpage) unless VALUE is a number
-            and the column type (see the dbdef_column manpage) does
-            not end in `char' or `binary'.
-
-    hfields TABLE
-            This is depriciated. Don't use it.
-
-            It returns a hash-type list with the fields of this
-            record's table set true.
-
-    fields TABLE
-            This returns a list of the columns in this record's
-            table (See the dbdef_table manpage).
-
-BUGS
-        This module should probably be renamed, since much of the
-        functionality is of general use. It is not completely unlike
-        Adapter::DBI (see below).
-
-        Exported qsearch and qsearchs should be depriciated in favor
-        of method calls (against an FS::Record object like the old
-        search and searchs that qsearch and qsearchs were on top
-        of.)
-
-        The whole fields / hfields mess should be removed.
-
-        The various WHERE clauses should be subroutined.
-
-        table string should be depriciated in favor of
-        FS::dbdef_table.
-
-        No doubt we could benefit from a Tied hash. Documenting how
-        exists / defined true maps to the database (and WHERE
-        clauses) would also help.
-
-        The ut_ methods should ask the dbdef for a default length.
-
-        ut_sqltype (like ut_varchar) should all be defined
-
-        A fallback check method should be provided with uses the
-        dbdef.
-
-        The ut_money method assumes money has two decimal digits.
-
-        The Pg money kludge in the new method only strips `$'.
-
-        The ut_phonen method assumes US-style phone numbers.
-
-        The _quote function should probably use ut_float instead of
-        a regex.
-
-        All the subroutines probably should be methods, here or
-        elsewhere.
-
-SEE ALSO
-        the FS::dbdef manpage, the FS::UID manpage, the DBI manpage
-
-        Adapter::DBI from Ch. 11 of Advanced Perl Programming by
-        Sriram Srinivasan.
-
-HISTORY
-        ivan@voicenet.com 97-jun-2 - 9, 19, 25, 27, 30
-
-        DBI version ivan@sisd.com 97-nov-8 - 12
-
-        cleaned up, added autoloaded $self->any_field calls, moved
-        DBI login stuff to FS::UID ivan@sisd.com 97-nov-21-23
-
-        since AUTO_INCREMENT is MySQL specific, use my own unique
-        number generator (again) ivan@sisd.com 97-dec-4
-
-        untaint $user in unique (web demo hack...bah) make unique
-        skip multiple-field unique's from dbdef ivan@sisd.com 97-
-        dec-11
-
-        merge with FS::Search, which after all was just alternate
-        constructors for FS::Record objects. Makes lots of things
-        cleaner. :) ivan@sisd.com 97-dec-13
-
-        use FS::dbdef::primary key in replace searches, hopefully
-        for all practical purposes the string/number problem in SQL
-        statements should be gone? (SQL bites) ivan@sisd.com 98-jan-
-        20
-
-        Put all SQL statments in $statment before we $sth=$dbh-
-        >prepare( them, for debugging reasons (warn $statement)
-        ivan@sisd.com 98-feb-19
-
-        (sigh)... use dbdef type (char, etc.) instead of a regex to
-        decide what to quote in _quote (more sillines...) SQL bites.
-        ivan@sisd.com 98-feb-20
-
-        more friendly error messages ivan@sisd.com 98-mar-13
-
-        Added import of datasrc from FS::UID to allow Pg6.3 to work
-        Added code to right-trim strings read from Pg6.3 databases
-        Modified 'add' to only insert fields that actually have data
-        Added ut_float to handle floating point numbers (for sales
-        tax). Pg6.3 does not have a "SHOW FIELDS" statement, so I
-        faked it 8). bmccane@maxbaud.net 98-apr-3
-
-        commented out Pg wrapper around `` Modified 'add' to only
-        insert fields that actually have data '' ivan@sisd.com 98-
-        apr-16
-
-        dbdef usage changes ivan@sisd.com 98-jun-1
-
-        sub fields now asks dbdef, not database ivan@sisd.com 98-
-        jun-2
-
-        added debugging method ->_dump ivan@sisd.com 98-jun-16
-
-        use FS::dbdef::primary key in delete searches as well as
-        replace searches (SQL still bites) ivan@sisd.com 98-jun-22
-
-        sub dbdef_table ivan@sisd.com 98-jun-28
-
-        removed Pg wrapper around `` Modified 'add' to only insert
-        fields that actually have data '' ivan@sisd.com 98-jul-14
-
-        sub fields croaks on errors ivan@sisd.com 98-jul-17
-
-        $rc eq '0E0' doesn't mean we couldn't delete for all rdbmss
-        ivan@sisd.com 98-jul-18
-
-        commented out code to right-trim strings read from Pg6.3
-        databases; ChopBlanks is in UID.pm ivan@sisd.com 98-aug-16
-
-        added code (with Pg wrapper) to deal with Pg money fields
-        ivan@sisd.com 98-aug-18
-
-        added pod documentation ivan@sisd.com 98-sep-6
-
diff --git a/htdocs/docs/man/SSH.txt b/htdocs/docs/man/SSH.txt
deleted file mode 100644 (file)
index b6d205b..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-NAME
-    FS::SSH - Subroutines to call ssh and scp
-
-SYNOPSIS
-      use FS::SSH qw(ssh scp issh iscp sshopen2 sshopen3);
-
-      ssh($host, $command);
-
-      issh($host, $command);
-
-      scp($source, $destination);
-
-      iscp($source, $destination);
-
-      sshopen2($host, $reader, $writer, $command);
-
-      sshopen3($host, $reader, $writer, $error, $command);
-
-DESCRIPTION
-      Simple wrappers around ssh and scp commands.
-
-SUBROUTINES
-    ssh HOST, COMMAND
-        Calls ssh in batch mode.
-
-    issh HOST, COMMAND
-        Prints the ssh command to be executed, waits for the user to
-        confirm, and (optionally) executes the command.
-
-    scp SOURCE, DESTINATION
-        Calls scp in batch mode.
-
-    iscp SOURCE, DESTINATION
-        Prints the scp command to be executed, waits for the user to
-        confirm, and (optionally) executes the command.
-
-    sshopen2 HOST, READER, WRITER, COMMAND
-        Connects the supplied filehandles to the ssh process (in
-        batch mode).
-
-    sshopen3 HOST, WRITER, READER, ERROR, COMMAND
-        Connects the supplied filehandles to the ssh process (in
-        batch mode).
-
-BUGS
-        Not OO.
-
-        scp stuff should transparantly use rsync-over-ssh instead.
-
-SEE ALSO
-        the ssh manpage, the scp manpage, the IPC::Open2 manpage,
-        the IPC::Open3 manpage
-
-HISTORY
-        ivan@voicenet.com 97-jul-17
-
-        added sshopen2 and sshopen3 ivan@sisd.com 98-mar-9
-
-        added iscp ivan@sisd.com 98-jul-25 now iscp asks y/n, issh
-        and took out path ivan@sisd.com 98-jul-30
-
-        pod ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/UID.txt b/htdocs/docs/man/UID.txt
deleted file mode 100644 (file)
index bf9f6b4..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-NAME
-    FS::UID - Subroutines for database login and assorted other
-    stuff
-
-SYNOPSIS
-      use FS::UID qw(adminsuidsetup cgisuidsetup dbh datasrc getotaker
-      checkeuid checkruid swapuid);
-
-      adminsuidsetup;
-
-      $cgi = new CGI::Base;
-      $cgi->get;
-      $dbh = cgisuidsetup($cgi);
-
-      $dbh = dbh;
-
-      $datasrc = datasrc;
-
-DESCRIPTION
-    Provides a hodgepodge of subroutines.
-
-SUBROUTINES
-    adminsuidsetup
-        Cleans the environment. Make sure the script is running as
-        freeside, or setuid freeside. Opens a connection to the
-        database. Swaps real and effective UIDs. Returns the DBI
-        database handle (usually you don't need this).
-
-    dbh Returns the DBI database handle.
-
-    datasrc
-        Returns the DBI data source.
-
-    getotaker
-        Returns the current Freeside user. Currently that means the
-        CGI REMOTE_USER, or 'freeside'.
-
-    checkeuid
-        Returns true if effective UID is that of the freeside user.
-
-    checkruid
-        Returns true if the real UID is that of the freeside user.
-
-    swapuid
-        Swaps real and effective UIDs.
-
-BUGS
-    Not OO.
-
-    No capabilities yet. When mod_perl and Authen::DBI are
-    implemented, cgisuidsetup will go away as well.
-
-SEE ALSO
-    the FS::Record manpage, the CGI::Base manpage, the DBI manpage
-
-HISTORY
-    ivan@voicenet.com 97-jun-4 - 9 untaint otaker ivan@voicenet.com
-    97-jul-7
-
-    generalize and auto-get uid (getotaker still needs to be db'ed)
-    ivan@sisd.com 97-nov-10
-
-    &cgisuidsetup logs into database. other cleaning. ivan@sisd.com
-    97-nov-22,23
-
-    &adminsuidsetup logs into database with otaker='freeside' (for
-    automated tasks like billing) ivan@sisd.com 97-dec-13
-
-    added sub datasrc for fs-setup ivan@sisd.com 98-feb-21
-
-    datasrc, user and pass now come from conf/secrets ivan@sisd.com
-    98-jun-28
-
-    added ChopBlanks to DBI call (see man DBI) ivan@sisd.com 98-aug-
-    16
-
-    pod, use FS::Conf, implemented cgisuidsetup as adminsuidsetup,
-    inlined suidsetup ivan@sisd.com 98-sep-12
-
diff --git a/htdocs/docs/man/agent.txt b/htdocs/docs/man/agent.txt
deleted file mode 100644 (file)
index b0317f6..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-NAME
-    FS::agent - Object methods for agent records
-
-SYNOPSIS
-      use FS::agent;
-
-      $record = create FS::agent \%hash;
-      $record = create FS::agent { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::agent object represents an agent. Every customer has an
-    agent. Agents can be used to track things like resellers or
-    salespeople. FS::agent inherits from FS::Record. The following
-    fields are currently supported:
-
-    agemtnum - primary key (assigned automatically for new agents)
-    agent - Text name of this agent
-    typenum - Agent type.  See the FS::agent_type manpage
-    prog - For future use.
-    freq - For future use.
-METHODS
-    create HASHREF
-        Creates a new agent. To add the agent to the database, see
-        the section on "insert".
-
-    insert
-        Adds this agent to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    delete
-        Deletes this agent from the database. Only agents with no
-        customers can be deleted. If there is an error, returns the
-        error, otherwise returns false.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    check
-        Checks all fields to make sure this is a valid agent. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-SEE ALSO
-    the FS::Record manpage, the FS::agent_type manpage, the
-    FS::cust_main manpage, schema.html from the base documentation.
-
-HISTORY
-    Class dealing with agent (resellers)
-
-    ivan@sisd.com 97-nov-13, 97-dec-10
-
-    pod, added check in ->delete ivan@sisd.com 98-sep-22
-
diff --git a/htdocs/docs/man/agent_type.txt b/htdocs/docs/man/agent_type.txt
deleted file mode 100644 (file)
index ea1edec..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-NAME
-    FS::agent_type - Object methods for agent_type records
-
-SYNOPSIS
-      use FS::agent_type;
-
-      $record = create FS::agent_type \%hash;
-      $record = create FS::agent_type { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::agent_type object represents an agent type. Every agent
-    (see the FS::agent manpage) has an agent type. Agent types
-    define which packages (see the FS::part_pkg manpage) may be
-    purchased by customers (see the FS::cust_main manpage), via
-    FS::type_pkgs records (see the FS::type_pkgs manpage).
-    FS::agent_type inherits from FS::Record. The following fields
-    are currently supported:
-
-    typenum - primary key (assigned automatically for new agent types)
-    atype - Text name of this agent type
-METHODS
-    create HASHREF
-        Creates a new agent type. To add the agent type to the
-        database, see the section on "insert".
-
-    insert
-        Adds this agent type to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    delete
-        Deletes this agent type from the database. Only agent types
-        with no agents can be deleted. If there is an error, returns
-        the error, otherwise returns false.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    check
-        Checks all fields to make sure this is a valid agent type.
-        If there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-SEE ALSO
-    the FS::Record manpage, the FS::agent manpage, the FS::type_pkgs
-    manpage, the FS::cust_main manpage, the FS::part_pkg manpage,
-    schema.html from the base documentation.
-
-HISTORY
-    Class for the different sets of allowable packages you can
-    assign to an agent.
-
-    ivan@sisd.com 97-nov-13
-
-    ut_ FS::Record methods ivan@sisd.com 97-dec-10
-
-    Changed 'type' to 'atype' because Pg6.3 reserves the type word
-    bmccane@maxbaud.net 98-apr-3
-
-    pod, added check in delete ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/cust_bill.txt b/htdocs/docs/man/cust_bill.txt
deleted file mode 100644 (file)
index 9762dd3..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-NAME
-    FS::cust_bill - Object methods for cust_bill records
-
-SYNOPSIS
-      use FS::cust_bill;
-
-      $record = create FS::cust_bill \%hash;
-      $record = create FS::cust_bill { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-      ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
-
-      @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
-
-      ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
-
-      @cust_pay_objects = $cust_bill->cust_pay;
-
-      @lines = $cust_bill->print_text;
-      @lines = $cust_bill->print_text $time;
-
-DESCRIPTION
-    An FS::cust_bill object represents an invoice. FS::cust_bill
-    inherits from FS::Record. The following fields are currently
-    supported:
-
-    invnum - primary key (assigned automatically for new invoices)
-    custnum - customer (see the FS::cust_main manpage)
-    _date - specified as a UNIX timestamp; see the section on "time" in the perlfunc manpage.  Also see
-    the Time::Local manpage and the Date::Parse manpage for conversion functions.
-    charged - amount of this invoice
-    owed - amount still outstanding on this invoice, which is charged minus
-    all payments (see the FS::cust_pay manpage).
-    printed - how many times this invoice has been printed automatically
-    (see the section on "collect" in the FS::cust_main manpage).
-METHODS
-    create HASHREF
-        Creates a new invoice. To add the invoice to the database,
-        see the section on "insert". Invoices are normally created
-        by calling the bill method of a customer object (see the
-        FS::cust_main manpage).
-
-    insert
-        Adds this invoice to the database ("Posts" the invoice). If
-        there is an error, returns the error, otherwise returns
-        false.
-
-        When adding new invoices, owed must be charged (or null, in
-        which case it is automatically set to charged).
-
-    delete
-        Currently unimplemented. I don't remove invoices because
-        there would then be no record you ever posted this invoice
-        (which is bad, no?)
-
-    replace OLD_RECORD
-        Replaces the OLD_RECORD with this one in the database. If
-        there is an error, returns the error, otherwise returns
-        false.
-
-        Only owed and printed may be changed. Owed is normally
-        updated by creating and inserting a payment (see the
-        FS::cust_pay manpage). Printed is normally updated by
-        calling the collect method of a customer object (see the
-        FS::cust_main manpage).
-
-    check
-        Checks all fields to make sure this is a valid invoice. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-    previous
-        Returns a list consisting of the total previous balance for
-        this customer, followed by the previous outstanding invoices
-        (as FS::cust_bill objects also).
-
-    cust_bill_pkg
-        Returns the line items (see the FS::cust_bill_pkg manpage)
-        for this invoice.
-
-    cust_credit
-        Returns a list consisting of the total previous credited
-        (see the FS::cust_credit manpage) for this customer,
-        followed by the previous outstanding credits
-        (FS::cust_credit objects).
-
-    cust_pay
-        Returns all payments (see the FS::cust_pay manpage) for this
-        invoice.
-
-    print_text [TIME];
-        Returns an ASCII invoice, as a list of lines.
-
-        TIME an optional value used to control the printing of
-        overdue messages. The default is now. It isn't the date of
-        the invoice; that's the `_date' field. It is specified as a
-        UNIX timestamp; see the section on "time" in the perlfunc
-        manpage. Also see the Time::Local manpage and the
-        Date::Parse manpage for conversion functions.
-
-BUGS
-    The delete method.
-
-    It doesn't properly override FS::Record yet.
-
-    print_text formatting (and some logic :/) is in source as a
-    format declaration, which needs to be slurped in from a file.
-    the fork is rather kludgy as well. It could be cleaned with
-    swrite from man perlform, and the picture could be put in a
-    /var/spool/freeside/conf file. Also number of lines ($=).
-
-    missing print_ps for a nice postscript copy (maybe HylaFAX-
-    cover-page-style or something similar so the look can be
-    completely customized?)
-
-    There is an off-by-one error in print_text which causes a visual
-    error: "Page 1 of 2" printed on some single-page invoices?
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_main manpage, the
-    FS::cust_pay manpage, the FS::cust_bill_pkg manpage, the
-    FS::cust_credit manpage, schema.html from the base
-    documentation.
-
-HISTORY
-    ivan@voicenet.com 97-jul-1
-
-    small fix for new API ivan@sisd.com 98-mar-14
-
-    charges can be negative ivan@sisd.com 98-jul-13
-
-    pod, ingegrate with FS::Invoice ivan@sisd.com 98-sep-20
-
diff --git a/htdocs/docs/man/cust_bill_pkg.txt b/htdocs/docs/man/cust_bill_pkg.txt
deleted file mode 100644 (file)
index 1ca4b8c..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-NAME
-    FS::cust_bill_pkg - Object methods for cust_bill_pkg records
-
-SYNOPSIS
-      use FS::cust_bill_pkg;
-
-      $record = create FS::cust_bill_pkg \%hash;
-      $record = create FS::cust_bill_pkg { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::cust_bill_pkg object represents an invoice line item.
-    FS::cust_bill_pkg inherits from FS::Record. The following fields
-    are currently supported:
-
-    invnum - invoice (see the FS::cust_bill manpage)
-    pkgnum - package (see the FS::cust_pkg manpage)
-    setup - setup fee
-    recur - recurring fee
-    sdate - starting date of recurring fee
-    edate - ending date of recurring fee
-    sdate and edate are specified as UNIX timestamps; see the
-    section on "time" in the perlfunc manpage. Also see the
-    Time::Local manpage and the Date::Parse manpage for conversion
-    functions.
-
-METHODS
-    create HASHREF
-        Creates a new line item. To add the line item to the
-        database, see the section on "insert". Line items are
-        normally created by calling the bill method of a customer
-        object (see the FS::cust_main manpage).
-
-    insert
-        Adds this line item to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    delete
-        Currently unimplemented. I don't remove line items because
-        there would then be no record the items ever existed (which
-        is bad, no?)
-
-    replace OLD_RECORD
-        Currently unimplemented. This would be even more of an
-        accounting nightmare than deleteing the items. Just don't do
-        it.
-
-    check
-        Checks all fields to make sure this is a valid line item. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert method.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_bill manpage, the
-    FS::cust_pkg manpage, the FS::cust_main manpage, schema.html
-    from the base documentation.
-
-HISTORY
-    ivan@sisd.com 98-mar-13
-
-    pod ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/cust_credit.txt b/htdocs/docs/man/cust_credit.txt
deleted file mode 100644 (file)
index 84591ee..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-NAME
-    FS::cust_credit - Object methods for cust_credit records
-
-SYNOPSIS
-      use FS::cust_credit;
-
-      $record = create FS::cust_credit \%hash;
-      $record = create FS::cust_credit { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::cust_credit object represents a credit. FS::cust_credit
-    inherits from FS::Record. The following fields are currently
-    supported:
-
-    crednum - primary key (assigned automatically for new credits)
-    custnum - customer (see the FS::cust_main manpage)
-    amount - amount of the credit
-    credited - how much of this credit that is still outstanding, which is
-    amount minus all refunds (see the FS::cust_refund manpage).
-    _date - specified as a UNIX timestamp; see the section on "time" in the perlfunc manpage.  Also see
-    the Time::Local manpage and the Date::Parse manpage for conversion functions.
-    otaker - order taker (assigned automatically, see the FS::UID manpage)
-    reason - text
-METHODS
-    create HASHREF
-        Creates a new credit. To add the credit to the database, see
-        the section on "insert".
-
-    insert
-        Adds this credit to the database ("Posts" the credit). If
-        there is an error, returns the error, otherwise returns
-        false.
-
-        When adding new invoices, credited must be amount (or null,
-        in which case it is automatically set to amount).
-
-    delete
-        Currently unimplemented.
-
-    replace OLD_RECORD
-        Replaces the OLD_RECORD with this one in the database. If
-        there is an error, returns the error, otherwise returns
-        false.
-
-        Only credited may be changed. Credited is normally updated
-        by creating and inserting a refund (see the FS::cust_refund
-        manpage).
-
-    check
-        Checks all fields to make sure this is a valid credit. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-BUGS
-    The delete method.
-
-    It doesn't properly override FS::Record yet.
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_refund manpage, the
-    FS::cust_bill manpage, schema.html from the base documentation.
-
-HISTORY
-    ivan@sisd.com 98-mar-17
-
-    pod, otaker from FS::UID ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/cust_main.txt b/htdocs/docs/man/cust_main.txt
deleted file mode 100644 (file)
index df78487..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-NAME
-    FS::cust_main - Object methods for cust_main records
-
-SYNOPSIS
-      use FS::cust_main;
-
-      $record = create FS::cust_main \%hash;
-      $record = create FS::cust_main { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-      @cust_pkg = $record->all_pkgs;
-
-      @cust_pkg = $record->ncancelled_pkgs;
-
-      $error = $record->bill;
-      $error = $record->bill %options;
-      $error = $record->bill 'time' => $time;
-
-      $error = $record->collect;
-      $error = $record->collect %options;
-      $error = $record->collect 'invoice_time'   => $time,
-                                'batch_card'     => 'yes',
-                                'report_badcard' => 'yes',
-                              ;
-
-DESCRIPTION
-    An FS::cust_main object represents a customer. FS::cust_main
-    inherits from FS::Record. The following fields are currently
-    supported:
-
-    custnum - primary key (assigned automatically for new customers)
-    agentnum - agent (see the FS::agent manpage)
-    refnum - referral (see the FS::part_referral manpage)
-    first - name
-    last - name
-    ss - social security number (optional)
-    company - (optional)
-    address1
-    address2 - (optional)
-    city
-    county - (optional, see the FS::cust_main_county manpage)
-    state - (see the FS::cust_main_county manpage)
-    zip
-    country - (see the FS::cust_main_county manpage)
-    daytime - phone (optional)
-    night - phone (optional)
-    payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
-    payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
-    paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
-    payname - name on card or billing name
-    tax - tax exempt, empty or `Y'
-    otaker - order taker (assigned automatically, see the FS::UID manpage)
-METHODS
-    create HASHREF
-        Creates a new customer. To add the customer to the database,
-        see the section on "insert".
-
-        Note that this stores the hash reference, not a distinct
-        copy of the hash it points to. You can ask the object for a
-        copy with the *hash* method.
-
-    insert
-        Adds this customer to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    delete
-        Currently unimplemented. Maybe cancel all of this customer's
-        packages (cust_pkg)?
-
-        I don't remove the customer record in the database because
-        there would then be no record the customer ever existed
-        (which is bad, no?)
-
-    replace OLD_RECORD
-        Replaces the OLD_RECORD with this one in the database. If
-        there is an error, returns the error, otherwise returns
-        false.
-
-    check
-        Checks all fields to make sure this is a valid customer
-        record. If there is an error, returns the error, otherwise
-        returns false. Called by the insert and repalce methods.
-
-    all_pkgs
-        Returns all packages (see the FS::cust_pkg manpage) for this
-        customer.
-
-    ncancelled_pkgs
-        Returns all non-cancelled packages (see the FS::cust_pkg
-        manpage) for this customer.
-
-    bill OPTIONS
-        Generates invoices (see the FS::cust_bill manpage) for this
-        customer. Usually used in conjunction with the collect
-        method.
-
-        The only currently available option is `time', which bills
-        the customer as if it were that time. It is specified as a
-        UNIX timestamp; see the section on "time" in the perlfunc
-        manpage). Also see the Time::Local manpage and the
-        Date::Parse manpage for conversion functions.
-
-        If there is an error, returns the error, otherwise returns
-        false.
-
-    collect OPTIONS
-        (Attempt to) collect money for this customer's outstanding
-        invoices (see the FS::cust_bill manpage). Usually used after
-        the bill method.
-
-        Depending on the value of `payby', this may print an invoice
-        (`BILL'), charge a credit card (`CARD'), or just add any
-        necessary (pseudo-)payment (`COMP').
-
-        If there is an error, returns the error, otherwise returns
-        false.
-
-        Currently available options are:
-
-        invoice_time - Use this time when deciding when to print
-        invoices and late notices on those invoices. The default is
-        now. It is specified as a UNIX timestamp; see the section on
-        "time" in the perlfunc manpage). Also see the Time::Local
-        manpage and the Date::Parse manpage for conversion
-        functions.
-
-        batch_card - Set this true to batch cards (see the
-        cust_pay_batch manpage). By default, cards are processed
-        immediately, which will generate an error if CyberCash is
-        not installed.
-
-        report_badcard - Set this true if you want bad card
-        transactions to return an error. By default, they don't.
-
-    total_owed
-        Returns the total owed for this customer on all invoices
-        (see the FS::cust_bill manpage).
-
-    total_credited
-        Returns the total credits (see the FS::cust_credit manpage)
-        for this customer.
-
-    balance
-        Returns the balance for this customer (total owed minus
-        total credited).
-
-BUGS
-    The delete method.
-
-    It doesn't properly override FS::Record yet.
-
-    hfields should be removed.
-
-    Bill and collect options should probably be passed as references
-    instead of a list.
-
-    CyberCash v2 forces us to define some variables in package main.
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_pkg manpage, the
-    FS::cust_bill manpage, the FS::cust_credit manpage the
-    FS::cust_pay_batch manpage, the FS::agent manpage, the
-    FS::part_referral manpage, the FS::cust_main_county manpage, the
-    FS::UID manpage, schema.html from the base documentation.
-
-HISTORY
-    ivan@voicenet.com 97-jul-28
-
-    Changed to standard Business::CreditCard no more TableUtil
-    EXPORT_OK FS::Record's hfields removed unique calls and locking
-    (not needed here now) wrapped the (now) optional fields in if
-    statements in sub check (notyetdone!) ivan@sisd.com 97-nov-12
-
-    updated paydate with SQL-type date info ivan@sisd.com 98-mar-5
-
-    Added export of datasrc from UID.pm for Pg6.3 changed 'day' to
-    'daytime' because Pg6.3 reserves the day word
-    bmccane@maxbaud.net 98-apr-3
-
-    in ->create, s/svc_acct/cust_main/, now it should actually
-    eliminate the warnings it was meant to ivan@sisd.com 98-jul-16
-
-    don't require a phone number and allow '/' in company names
-    ivan@sisd.com 98-jul-18
-
-    use ut_ and rewrite &check, &*_pkgs ivan@sisd.com 98-sep-5
-
-    pod, merge with FS::Bill (about time!), total_owed,
-    total_credited and balance methods, cleaned collect method,
-    source modifications no longer necessary to enable cybercash,
-    cybercash v3 support, don't need to import
-    FS::UID::{datasrc,checkruid} ivan@sisd.com 98-sep-19-21
-
diff --git a/htdocs/docs/man/cust_main_county.txt b/htdocs/docs/man/cust_main_county.txt
deleted file mode 100644 (file)
index 8e99397..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-NAME
-    FS::cust_main_county - Object methods for cust_main_county
-    objects
-
-SYNOPSIS
-      use FS::cust_main_county;
-
-      $record = create FS::cust_main_county \%hash;
-      $record = create FS::cust_main_county { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::cust_main_county object represents a tax rate, defined by
-    locale. FS::cust_main_county inherits from FS::Record. The
-    following fields are currently supported:
-
-    taxnum - primary key (assigned automatically for new tax rates)
-    state
-    county
-    tax - percentage
-METHODS
-    create HASHREF
-        Creates a new tax rate. To add the tax rate to the database,
-        see the section on "insert".
-
-    insert
-        Adds this tax rate to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    delete
-        Deletes this tax rate from the database. If there is an
-        error, returns the error, otherwise returns false.
-
-    replace OLD_RECORD
-        Replaces the OLD_RECORD with this one in the database. If
-        there is an error, returns the error, otherwise returns
-        false.
-
-    check
-        Checks all fields to make sure this is a valid tax rate. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    A country field (and possibly a currency field) should be added.
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_main manpage, the
-    FS::cust_bill manpage, schema.html from the base documentation.
-
-HISTORY
-    ivan@voicenet.com 97-dec-16
-
-    Changed check for 'tax' to use the new ut_float subroutine
-    bmccane@maxbaud.net 98-apr-3
-
-    pod ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/cust_pay.txt b/htdocs/docs/man/cust_pay.txt
deleted file mode 100644 (file)
index 9f28d08..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-NAME
-    FS::cust_pay - Object methods for cust_pay objects
-
-SYNOPSIS
-      use FS::cust_pay;
-
-      $record = create FS::cust_pay \%hash;
-      $record = create FS::cust_pay { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::cust_pay object represents a payment. FS::cust_pay
-    inherits from FS::Record. The following fields are currently
-    supported:
-
-    paynum - primary key (assigned automatically for new payments)
-    invnum - Invoice (see the FS::cust_bill manpage)
-    paid - Amount of this payment
-    _date - specified as a UNIX timestamp; see the section on "time" in the perlfunc manpage.  Also see
-    the Time::Local manpage and the Date::Parse manpage for conversion functions.
-    payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
-    payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
-    paybatch - text field for tracking card processing
-METHODS
-    create HASHREF
-        Creates a new payment. To add the payment to the databse,
-        see the section on "insert".
-
-    insert
-        Adds this payment to the databse, and updates the invoice
-        (see the FS::cust_bill manpage).
-
-    delete
-        Currently unimplemented (accounting reasons).
-
-    replace OLD_RECORD
-        Currently unimplemented (accounting reasons).
-
-    check
-        Checks all fields to make sure this is a valid payment. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert method.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    Delete and replace methods.
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_bill manpage, schema.html
-    from the base documentation.
-
-HISTORY
-    ivan@voicenet.com 97-jul-1 - 25 - 29
-
-    new api ivan@sisd.com 98-mar-13
-
-    pod ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/cust_pkg.txt b/htdocs/docs/man/cust_pkg.txt
deleted file mode 100644 (file)
index 5409083..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-NAME
-    FS::cust_pkg - Object methods for cust_pkg objects
-
-SYNOPSIS
-      use FS::cust_pkg;
-
-      $record = create FS::cust_pkg \%hash;
-      $record = create FS::cust_pkg { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-      $error = $record->cancel;
-
-      $error = $record->suspend;
-
-      $error = $record->unsuspend;
-
-      $error = FS::cust_pkg::order( $custnum, \@pkgparts );
-      $error = FS::cust_pkg::order( $custnum, \@pkgparts, \@remove_pkgnums ] );
-
-DESCRIPTION
-    An FS::cust_pkg object represents a customer billing item.
-    FS::cust_pkg inherits from FS::Record. The following fields are
-    currently supported:
-
-    pkgnum - primary key (assigned automatically for new billing items)
-    custnum - Customer (see the FS::cust_main manpage)
-    pkgpart - Billing item definition (see the FS::part_pkg manpage)
-    setup - date
-    bill - date
-    susp - date
-    expire - date
-    cancel - date
-    otaker - order taker (assigned automatically if null, see the FS::UID manpage)
-    Note: setup, bill, susp, expire and cancel are specified as UNIX
-    timestamps; see the section on "time" in the perlfunc manpage.
-    Also see the Time::Local manpage and the Date::Parse manpage for
-    conversion functions.
-
-METHODS
-    create HASHREF
-        Create a new billing item. To add the item to the database,
-        see the section on "insert".
-
-    insert
-        Adds this billing item to the database ("Orders" the item).
-        If there is an error, returns the error, otherwise returns
-        false.
-
-    delete
-        Currently unimplemented. You don't want to delete billing
-        items, because there would then be no record the customer
-        ever purchased the item. Instead, see the cancel method.
-
-        sub delete { return "Can't delete cust_pkg records!"; }
-
-    replace OLD_RECORD
-        Replaces the OLD_RECORD with this one in the database. If
-        there is an error, returns the error, otherwise returns
-        false.
-
-        Currently, custnum, setup, bill, susp, expire, and cancel
-        may be changed.
-
-        pkgpart may not be changed, but see the order subroutine.
-
-        setup and bill are normally updated by calling the bill
-        method of a customer object (see the FS::cust_main manpage).
-
-        suspend is normally updated by the suspend and unsuspend
-        methods.
-
-        cancel is normally updated by the cancel method (and also
-        the order subroutine in some cases).
-
-    check
-        Checks all fields to make sure this is a valid billing item.
-        If there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-    cancel
-        Cancels and removes all services (see the FS::cust_svc
-        manpage and the FS::part_svc manpage) in this package, then
-        cancels the package itself (sets the cancel field to now).
-
-        If there is an error, returns the error, otherwise returns
-        false.
-
-    suspend
-        Suspends all services (see the FS::cust_svc manpage and the
-        FS::part_svc manpage) in this package, then suspends the
-        package itself (sets the susp field to now).
-
-        If there is an error, returns the error, otherwise returns
-        false.
-
-    unsuspend
-        Unsuspends all services (see the FS::cust_svc manpage and
-        the FS::part_svc manpage) in this package, then unsuspends
-        the package itself (clears the susp field).
-
-        If there is an error, returns the error, otherwise returns
-        false.
-
-SUBROUTINES
-    order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF ]
-        CUSTNUM is a customer (see the FS::cust_main manpage)
-
-        PKGPARTS is a list of pkgparts specifying the the billing
-        item definitions (see the FS::part_pkg manpage) to order for
-        this customer. Duplicates are of course permitted.
-
-        REMOVE_PKGNUMS is an optional list of pkgnums specifying the
-        billing items to remove for this customer. The services (see
-        the FS::cust_svc manpage) are moved to the new billing
-        items. An error is returned if this is not possible (see the
-        FS::pkg_svc manpage).
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    sub order is not OO. Perhaps it should be moved to FS::cust_main
-    and made so?
-
-    In sub order, the @pkgparts array (passed by reference) is
-    clobbered.
-
-    Also in sub order, no money is adjusted. Once FS::part_pkg
-    defines a standard method to pass dates to the recur_prog
-    expression, it should do so.
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_main manpage, the
-    FS::part_pkg manpage, the FS::cust_svc manpage , the FS::pkg_svc
-    manpage, schema.html from the base documentation
-
-HISTORY
-    ivan@voicenet.com 97-jul-1 - 21
-
-    fixed for new agent->agent_type->type_pkgs in &order
-    ivan@sisd.com 98-mar-7
-
-    pod ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/cust_refund.txt b/htdocs/docs/man/cust_refund.txt
deleted file mode 100644 (file)
index 392a0b5..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-NAME
-    FS::cust_refund - Object method for cust_refund objects
-
-SYNOPSIS
-      use FS::cust_refund;
-
-      $record = create FS::cust_refund \%hash;
-      $record = create FS::cust_refund { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::cust_refund represents a refund. FS::cust_refund inherits
-    from FS::Record. The following fields are currently supported:
-
-    refundnum - primary key (assigned automatically for new refunds)
-    crednum - Credit (see the FS::cust_credit manpage)
-    refund - Amount of the refund
-    _date - specified as a UNIX timestamp; see the section on "time" in the perlfunc manpage.  Also see
-    the Time::Local manpage and the Date::Parse manpage for conversion functions.
-    payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
-    payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
-    otaker - order taker (assigned automatically, see the FS::UID manpage)
-METHODS
-    create HASHREF
-        Creates a new refund. To add the refund to the database, see
-        the section on "insert".
-
-    insert
-        Adds this refund to the database, and updates the credit
-        (see the FS::cust_credit manpage).
-
-    delete
-        Currently unimplemented (accounting reasons).
-
-    replace OLD_RECORD
-        Currently unimplemented (accounting reasons).
-
-    check
-        Checks all fields to make sure this is a valid refund. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert method.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    Delete and replace methods.
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_credit manpage, schema.html
-    from the base documentation.
-
-HISTORY
-    ivan@sisd.com 98-mar-18
-
-    ->create had wrong tablename ivan@sisd.com 98-jun-16 (finish
-    me!)
-
-    pod and finish up ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/cust_svc.txt b/htdocs/docs/man/cust_svc.txt
deleted file mode 100644 (file)
index d863ea8..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-NAME
-    FS::cust_svc - Object method for cust_svc objects
-
-SYNOPSIS
-      use FS::cust_svc;
-
-      $record = create FS::cust_svc \%hash
-      $record = create FS::cust_svc { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::cust_svc represents a service. FS::cust_svc inherits from
-    FS::Record. The following fields are currently supported:
-
-    svcnum - primary key (assigned automatically for new services)
-    pkgnum - Package (see the FS::cust_pkg manpage)
-    svcpart - Service definition (see the FS::part_svc manpage)
-METHODS
-    create HASHREF
-        Creates a new service. To add the refund to the database,
-        see the section on "insert". Services are normally created
-        by creating FS::svc_ objects (see the FS::svc_acct manpage,
-        the FS::svc_domain manpage, and the FS::svc_acct_sm manpage,
-        among others).
-
-    insert
-        Adds this service to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    delete
-        Deletes this service from the database. If there is an
-        error, returns the error, otherwise returns false.
-
-        Called by the cancel method of the package (see the
-        FS::cust_pkg manpage).
-
-    replace OLD_RECORD
-        Replaces the OLD_RECORD with this one in the database. If
-        there is an error, returns the error, otherwise returns
-        false.
-
-    check
-        Checks all fields to make sure this is a valid service. If
-        there is an error, returns the error, otehrwise returns
-        false. Called by the insert and replace methods.
-
-BUGS
-    Behaviour of changing the svcpart of cust_svc records is
-    undefined and should possibly be prohibited, and pkg_svc records
-    are not checked.
-
-    pkg_svc records are not checket in general (here).
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_pkg manpage, the
-    FS::part_svc manpage, the FS::pkg_svc manpage, schema.html from
-    the base documentation
-
-HISTORY
-    ivan@voicenet.com 97-jul-10,14
-
-    no TableUtil, no FS::Lock ivan@sisd.com 98-mar-7
-
-    pod ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/dbdef.txt b/htdocs/docs/man/dbdef.txt
deleted file mode 100644 (file)
index 6f1215a..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-NAME
-    FS::dbdef - Database objects
-
-SYNOPSIS
-      use FS::dbdef;
-
-      $dbdef = new FS::dbdef (@dbdef_table_objects);
-      $dbdef = load FS::dbdef "filename";
-
-      $dbdef->save("filename");
-
-      $dbdef->addtable($dbdef_table_object);
-
-      @table_names = $dbdef->tables;
-
-      $FS_dbdef_table_object = $dbdef->table;
-
-DESCRIPTION
-    FS::dbdef objects are collections of FS::dbdef_table objects and
-    represnt a database (a collection of tables).
-
-METHODS
-    new TABLE, TABLE, ...
-        Creates a new FS::dbdef object
-
-    load FILENAME
-        Loads an FS::dbdef object from a file.
-
-    save FILENAME
-        Saves an FS::dbdef object to a file.
-
-    addtable TABLE
-        Adds this FS::dbdef_table object.
-
-    tables
-        Returns the names of all tables.
-
-    table TABLENAME
-        Returns the named FS::dbdef_table object.
-
-BUGS
-        Each FS::dbdef object should have a name which corresponds
-        to its name within the SQL database engine.
-
-SEE ALSO
-        the FS::dbdef_table manpage, the FS::Record manpage,
-
-HISTORY
-        beginning of abstraction into a class (not really)
-
-        ivan@sisd.com 97-dec-4
-
-        added primary_key ivan@sisd.com 98-jan-20
-
-        added datatype (very kludgy and needs to be cleaned)
-        ivan@sisd.com 98-feb-21
-
-        perltrap (sigh) masked by mysql 3.20->3,21 ivan@sisd.com 98-
-        mar-2
-
-        Change 'type' to 'atype' in agent_type Changed attributes to
-        special words which are changed in fs-setup ie. double(10,2)
-        <=> MONEYTYPE Changed order of some of the field definitions
-        because Pg6.3 is picky Changed 'day' to 'daytime' in
-        cust_main Changed type of tax from tinyint to real Change
-        'password' to '_password' in svc_acct Pg6.3 does not allow
-        'field char(x) NULL' bmccane@maxbaud.net 98-apr-3
-
-        rewrite: now properly OO. See also
-        FS::dbdef_{table,column,unique,index}
-
-        ivan@sisd.com 98-apr-17
-
-        gained some extra functions ivan@sisd.com 98-may-11
-
-        now knows how to Freeze and Thaw itself ivan@sisd.com 98-
-        jun-2
-
-        pod ivan@sisd.com 98-sep-23
-
diff --git a/htdocs/docs/man/dbdef_colgroup.txt b/htdocs/docs/man/dbdef_colgroup.txt
deleted file mode 100644 (file)
index a7eebc6..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-NAME
-    FS::dbdef_colgroup - Column group objects
-
-SYNOPSIS
-      use FS::dbdef_colgroup;
-
-      $colgroup = new FS::dbdef_colgroup ( $lol );
-      $colgroup = new FS::dbdef_colgroup (
-        [
-          [ 'single_column' ],
-          [ 'multiple_columns', 'another_column', ],
-        ]
-      );
-
-      @sql_lists = $colgroup->sql_list;
-
-      @singles = $colgroup->singles;
-
-DESCRIPTION
-    FS::dbdef_colgroup objects represent sets of sets of columns.
-
-METHODS
-    new Creates a new FS::dbdef_colgroup object.
-
-    sql_list
-        Returns a flat list of comma-separated values, for SQL
-        statements.
-
-    singles
-        Returns a flat list of all single item lists.
-
-BUGS
-SEE ALSO
-    the FS::dbdef_table manpage, the FS::dbdef_unique manpage, the
-    FS::dbdef_index manpage, the FS::dbdef_column manpage, the
-    FS::dbdef manpage, the perldsc manpage
-
-HISTORY
-    class for dealing with groups of groups of columns (used as a
-    base class by FS::dbdef_{unique,index} )
-
-    ivan@sisd.com 98-apr-19
-
-    added singles, fixed sql_list to skip empty lists ivan@sisd.com
-    98-jun-2
-
-    untaint things we're returning in sub singels ivan@sisd.com 98-
-    jun-4
-
-    pod ivan@sisd.com 98-sep-24
-
diff --git a/htdocs/docs/man/dbdef_column.txt b/htdocs/docs/man/dbdef_column.txt
deleted file mode 100644 (file)
index 93e2395..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-NAME
-    FS::dbdef_column - Column object
-
-SYNOPSIS
-      use FS::dbdef_column;
-
-      $column_object = new FS::dbdef_column ( $name, $sql_type, '' );
-      $column_object = new FS::dbdef_column ( $name, $sql_type, 'NULL' );
-      $column_object = new FS::dbdef_column ( $name, $sql_type, '', $length );
-      $column_object = new FS::dbdef_column ( $name, $sql_type, 'NULL', $length );
-
-      $name = $column_object->name;
-      $column_object->name ( 'name' );
-
-      $name = $column_object->type;
-      $column_object->name ( 'sql_type' );
-
-      $name = $column_object->null;
-      $column_object->name ( 'NOT NULL' );
-
-      $name = $column_object->length;
-      $column_object->name ( $length );
-
-      $sql_line = $column->line;
-      $sql_line = $column->line $datasrc;
-
-DESCRIPTION
-    FS::dbdef::column objects represend columns in tables (see the
-    FS::dbdef_table manpage).
-
-METHODS
-    new Creates a new FS::dbdef_column object.
-
-    name
-        Returns or sets the column name.
-
-    type
-        Returns or sets the column type.
-
-    null
-        Returns or sets the column null flag.
-
-    type
-        Returns or sets the column length.
-
-    line [ $datasrc ]
-        Returns an SQL column definition.
-
-        If passed a DBI $datasrc specifying the DBD::mysql manpage,
-        will use MySQL-specific syntax. Non-standard syntax for
-        other engines (if applicable) may also be supported in the
-        future.
-
-BUGS
-SEE ALSO
-    the FS::dbdef_table manpage, the FS::dbdef manpage, the DBI
-    manpage
-
-HISTORY
-    class for dealing with column definitions
-
-    ivan@sisd.com 98-apr-17
-
-    now methods can be used to get or set data ivan@sisd.com 98-may-
-    11
-
-    mySQL-specific hack for null (what should be default?)
-    ivan@sisd.com 98-jun-2
-
diff --git a/htdocs/docs/man/dbdef_index.txt b/htdocs/docs/man/dbdef_index.txt
deleted file mode 100644 (file)
index 8cf339b..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-NAME
-    FS::dbdef_unique.pm - Index object
-
-SYNOPSIS
-      use FS::dbdef_index;
-
-        # see FS::dbdef_colgroup methods
-
-DESCRIPTION
-    FS::dbdef_unique objects represent the (non-unique) indices of a
-    table (the FS::dbdef_table manpage). FS::dbdef_unique inherits
-    from FS::dbdef_colgroup.
-
-BUGS
-    Is this empty subclass needed?
-
-SEE ALSO
-    the FS::dbdef_colgroup manpage, the FS::dbdef_record manpage,
-    the FS::Record manpage
-
-HISTORY
-    class for dealing with index definitions
-
-    ivan@sisd.com 98-apr-19
-
-    pod ivan@sisd.com 98-sep-24
-
diff --git a/htdocs/docs/man/dbdef_table.txt b/htdocs/docs/man/dbdef_table.txt
deleted file mode 100644 (file)
index 25e010d..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-NAME
-    FS::dbdef_table - Table objects
-
-SYNOPSIS
-      use FS::dbdef_table;
-
-      $dbdef_table = new FS::dbdef_table (
-        "table_name",
-        "primary_key",
-        $FS_dbdef_unique_object,
-        $FS_dbdef_index_object,
-        @FS_dbdef_column_objects,
-      );
-
-      $dbdef_table->addcolumn ( $FS_dbdef_column_object );
-
-      $table_name = $dbdef_table->name;
-      $dbdef_table->name ("table_name");
-
-      $table_name = $dbdef_table->primary_keye;
-      $dbdef_table->primary_key ("primary_key");
-
-      $FS_dbdef_unique_object = $dbdef_table->unique;
-      $dbdef_table->unique ( $FS_dbdef_unique_object );
-
-      $FS_dbdef_index_object = $dbdef_table->index;
-      $dbdef_table->index ( $FS_dbdef_index_object );
-
-      @column_names = $dbdef->columns;
-
-      $FS_dbdef_column_object = $dbdef->column;
-
-      @sql_statements = $dbdef->sql_create_table;
-      @sql_statements = $dbdef->sql_create_table $datasrc;
-
-DESCRIPTION
-    FS::dbdef_table objects represent a single database table.
-
-METHODS
-    new Creates a new FS::dbdef_table object.
-
-    addcolumn
-        Adds this FS::dbdef_column object.
-
-    name
-        Returns or sets the table name.
-
-    primary_key
-        Returns or sets the primary key.
-
-    unique
-        Returns or sets the FS::dbdef_unique object.
-
-    index
-        Returns or sets the FS::dbdef_index object.
-
-    columns
-        Returns a list consisting of the names of all columns.
-
-    column "column"
-        Returns the column object (see the FS::dbdef_column manpage)
-        for "column".
-
-    sql_create_table [ $datasrc ]
-        Returns an array of SQL statments to create this table.
-
-        If passed a DBI $datasrc specifying the DBD::mysql manpage,
-        will use MySQL-specific syntax. Non-standard syntax for
-        other engines (if applicable) may also be supported in the
-        future.
-
-BUGS
-SEE ALSO
-    the FS::dbdef manpage, the FS::dbdef_unique manpage, the
-    FS::dbdef_index manpage, the FS::dbdef_unique manpage, the DBI
-    manpage
-
-HISTORY
-    class for dealing with table definitions
-
-    ivan@sisd.com 98-apr-18
-
-    gained extra functions (should %columns be an IxHash?)
-    ivan@sisd.com 98-may-11
-
-    sql_create_table returns a list of statments, not just one, and
-    now it does indices (plus mysql hack) ivan@sisd.com 98-jun-2
-
-    untaint primary_key... hmm. is this a hack around a bigger
-    problem? looks like, did the same thing singles in colgroup!
-    ivan@sisd.com 98-jun-4
-
-    pod ivan@sisd.com 98-sep-24
-
diff --git a/htdocs/docs/man/dbdef_unique.txt b/htdocs/docs/man/dbdef_unique.txt
deleted file mode 100644 (file)
index 0e4f015..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-NAME
-    FS::dbdef_unique.pm - Unique object
-
-SYNOPSIS
-      use FS::dbdef_unique;
-
-      # see FS::dbdef_colgroup methods
-
-DESCRIPTION
-    FS::dbdef_unique objects represent the unique indices of a
-    database table (the FS::dbdef_table manpage). FS::dbdef_unique
-    inherits from FS::dbdef_colgroup.
-
-BUGS
-    Is this empty subclass needed?
-
-SEE ALSO
-    the FS::dbdef_colgroup manpage, the FS::dbdef_record manpage,
-    the FS::Record manpage
-
-HISTORY
-    class for dealing with unique definitions
-
-    ivan@sisd.com 98-apr-19
-
-    pod ivan@sisd.com 98-sep-24
-
diff --git a/htdocs/docs/man/index.html b/htdocs/docs/man/index.html
deleted file mode 100644 (file)
index 4f33dd4..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-<head>
-  <title>Perl API</title>
-</head>
-<body>
-  <h1>Perl API</h1>
-  <ul>
-<li><a href="agent.txt">FS::agent</a>
-<li><a href="agent_type.txt">FS::agent_type</a>
-<li><a href="cust_bill.txt">FS::cust_bill</a>
-<li><a href="cust_bill_pkg.txt">FS::cust_bill_pkg</a>
-<li><a href="cust_credit.txt">FS::cust_credit</a>
-<li><a href="cust_main.txt">FS::cust_main</a>
-<li><a href="cust_main_county.txt">FS::cust_main_county</a>
-<li><a href="cust_pay.txt">FS::cust_pay</a>
-<li><a href="cust_pkg.txt">FS::cust_pkg</a>
-<li><a href="cust_refund.txt">FS::cust_refund</a>
-<li><a href="cust_svc.txt">FS::cust_svc</a>
-<li><a href="part_pkg.txt">FS::part_pkg</a>
-<li><a href="part_referral.txt">FS::part_referral</a>
-<li><a href="part_svc.txt">FS::part_svc</a>
-<li><a href="pkg_svc.txt">FS::pkg_svc</a>
-<li><a href="svc_acct.txt">FS::svc_acct</a>
-<li><a href="svc_acct_pop.txt">FS::svc_acct_pop</a>
-<li><a href="svc_acct_sm.txt">FS::svc_acct_sm</a>
-<li><a href="svc_domain.txt">FS::svc_domain</a>
-<li><a href="type_pkgs.txt">FS::type_pkgs</a>
-</ul>
-<br>
-<ul>
-<li><a href="Bill.txt">FS::Bill</a>
-<li><a href="CGI.txt">FS::CGI</a>
-<li><a href="Conf.txt">FS::Conf</a>
-<li><a href="Invoice.txt">FS::Invoice</a>
-<li><a href="Record.txt">FS::Record</a>
-<li><a href="SSH.txt">FS::SSH</a>
-<li><a href="UID.txt">FS::UID</a>
-</ul>
-<br>
-<ul>
-<li><a href="dbdef.txt">FS::dbdef</a>
-<li><a href="dbdef_colgroup.txt">FS::dbdef_colgroup</a>
-<li><a href="dbdef_column.txt">FS::dbdef_column</a>
-<li><a href="dbdef_index.txt">FS::dbdef_index</a>
-<li><a href="dbdef_table.txt">FS::dbdef_table</a>
-<li><a href="dbdef_unique.txt">FS::dbdef_unique</a>
-
-<ul>
-</body>
diff --git a/htdocs/docs/man/part_pkg.txt b/htdocs/docs/man/part_pkg.txt
deleted file mode 100644 (file)
index dc1bce4..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-NAME
-    FS::part_pkg - Object methods for part_pkg objects
-
-SYNOPSIS
-      use FS::part_pkg;
-
-      $record = create FS::part_pkg \%hash
-      $record = create FS::part_pkg { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::part_pkg represents a billing item definition.
-    FS::part_pkg inherits from FS::Record. The following fields are
-    currently supported:
-
-    pkgpart - primary key (assigned automatically for new billing item definitions)
-    pkg - Text name of this billing item definition (customer-viewable)
-    comment - Text name of this billing item definition (non-customer-viewable)
-    setup - Setup fee
-    freq - Frequency of recurring fee
-    recur - Recurring fee
-    setup and recur are evaluated as Safe perl expressions. You can
-    use numbers just as you would normally. More advanced semantics
-    are not yet defined.
-
-METHODS
-    create HASHREF
-        Creates a new billing item definition. To add the billing
-        item definition to the database, see the section on
-        "insert".
-
-    insert
-        Adds this billing item definition to the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    delete
-        Currently unimplemented.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    check
-        Checks all fields to make sure this is a valid billing item
-        definition. If there is an error, returns the error,
-        otherwise returns false. Called by the insert and replace
-        methods.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    The delete method is unimplemented.
-
-    setup and recur semantics are not yet defined (and are
-    implemented in FS::cust_bill. hmm.).
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_pkg manpage, the
-    FS::type_pkgs manpage, the FS::pkg_svc manpage, the Safe
-    manpage. schema.html from the base documentation.
-
-HISTORY
-    ivan@sisd.com 97-dec-5
-
-    pod ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/part_referral.txt b/htdocs/docs/man/part_referral.txt
deleted file mode 100644 (file)
index 5349963..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-NAME
-    FS::part_referral - Object methods for part_referral objects
-
-SYNOPSIS
-      use FS::part_referral;
-
-      $record = create FS::part_referral \%hash
-      $record = create FS::part_referral { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::part_referral represents a referral - where a customer
-    heard of your services. This can be used to track the
-    effectiveness of a particular piece of advertising, for example.
-    FS::part_referral inherits from FS::Record. The following fields
-    are currently supported:
-
-    refnum - primary key (assigned automatically for new referrals)
-    referral - Text name of this referral
-METHODS
-    create HASHREF
-        Creates a new referral. To add the referral to the database,
-        see the section on "insert".
-
-    insert
-        Adds this referral to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    delete
-        Currently unimplemented.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    check
-        Checks all fields to make sure this is a valid referral. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    The delete method is unimplemented.
-
-SEE ALSO
-    the FS::Record manpage, the FS::cust_main manpage, schema.html
-    from the base documentation.
-
-HISTORY
-    Class dealing with referrals
-
-    ivan@sisd.com 98-feb-23
-
-    pod ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/part_svc.txt b/htdocs/docs/man/part_svc.txt
deleted file mode 100644 (file)
index 680944e..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-NAME
-    FS::part_svc - Object methods for part_svc objects
-
-SYNOPSIS
-      use FS::part_svc;
-
-      $record = create FS::part_referral \%hash
-      $record = create FS::part_referral { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::part_svc represents a service definition. FS::part_svc
-    inherits from FS::Record. The following fields are currently
-    supported:
-
-    svcpart - primary key (assigned automatically for new service definitions)
-    svc - text name of this service definition
-    svcdb - table used for this service.  See the FS::svc_acct manpage,
-    the FS::svc_domain manpage, and the FS::svc_acct_sm manpage, among others.
-    *svcdb*__*field* - Default or fixed value for *field* in *svcdb*.
-    *svcdb*__*field*_flag - defines *svcdb*__*field* action: null, `D' for default, or `F' for fixed
-METHODS
-    create HASHREF
-        Creates a new service definition. To add the service
-        definition to the database, see the section on "insert".
-
-    insert
-        Adds this service definition to the database. If there is an
-        error, returns the error, otherwise returns false.
-
-    delete
-        Currently unimplemented.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    check
-        Checks all fields to make sure this is a valid service
-        definition. If there is an error, returns the error,
-        otherwise returns false. Called by the insert and replace
-        methods.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    Delete is unimplemented.
-
-SEE ALSO
-    the FS::Record manpage, the FS::part_pkg manpage, the
-    FS::pkg_svc manpage, the FS::cust_svc manpage, the FS::svc_acct
-    manpage, the FS::svc_acct_sm manpage, the FS::svc_domain
-    manpage, schema.html from the base documentation.
-
-HISTORY
-    ivan@sisd.com 97-nov-14
-
-    data checking/untainting calls into FS::Record added
-    ivan@sisd.com 97-dec-6
-
-    pod ivan@sisd.com 98-sep-21
-
diff --git a/htdocs/docs/man/pkg_svc.txt b/htdocs/docs/man/pkg_svc.txt
deleted file mode 100644 (file)
index bde0043..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-NAME
-    FS::pkg_svc - Object methods for pkg_svc records
-
-SYNOPSIS
-      use FS::pkg_svc;
-
-      $record = create FS::pkg_svc \%hash;
-      $record = create FS::pkg_svc { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::pkg_svc record links a billing item definition (see the
-    FS::part_pkg manpage) to a service definition (see the
-    FS::part_svc manpage). FS::pkg_svc inherits from FS::Record. The
-    following fields are currently supported:
-
-    pkgpart - Billing item definition (see the FS::part_pkg manpage)
-    svcpart - Service definition (see the FS::part_svc manpage)
-    quantity - Quantity of this service definition that this billing item
-    definition includes
-METHODS
-    create HASHREF
-        Create a new record. To add the record to the database, see
-        the section on "insert".
-
-    insert
-        Adds this record to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    delete
-        Deletes this record from the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    check
-        Checks all fields to make sure this is a valid record. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-SEE ALSO
-    the FS::Record manpage, the FS::part_pkg manpage, the
-    FS::part_svc manpage, schema.html from the base documentation.
-
-HISTORY
-    ivan@voicenet.com 97-jul-1 added hfields ivan@sisd.com 97-nov-13
-
-    pod ivan@sisd.com 98-sep-22
-
diff --git a/htdocs/docs/man/svc_acct.txt b/htdocs/docs/man/svc_acct.txt
deleted file mode 100644 (file)
index 1c9caf5..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-NAME
-    FS::svc_acct - Object methods for svc_acct records
-
-SYNOPSIS
-      use FS::svc_acct;
-
-      $record = create FS::svc_acct \%hash;
-      $record = create FS::svc_acct { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-      $error = $record->suspend;
-
-      $error = $record->unsuspend;
-
-      $error = $record->cancel;
-
-DESCRIPTION
-    An FS::svc_acct object represents an account. FS::svc_acct
-    inherits from FS::Record. The following fields are currently
-    supported:
-
-    svcnum - primary key (assigned automatcially for new accounts)
-    username
-    _password - generated if blank
-    popnum - Point of presence (see the FS::svc_acct_pop manpage)
-    uid
-    gid
-    finger - GECOS
-    dir - set automatically if blank (and uid is not)
-    shell
-    quota - (unimplementd)
-    slipip - IP address
-    radius_*Radius_Attribute* - *Radius-Attribute*
-METHODS
-    create HASHREF
-        Creates a new account. To add the account to the database,
-        see the section on "insert".
-
-    insert
-        Adds this account to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-        The additional fields pkgnum and svcpart (see the
-        FS::cust_svc manpage) should be defined. An FS::cust_svc
-        record will be created and inserted.
-
-        If the configuration value (see the FS::Conf manpage)
-        shellmachine exists, and the username, uid, and dir fields
-        are defined, the command
-
-          useradd -d $dir -m -s $shell -u $uid $username
-
-        is executed on shellmachine via ssh. This behaviour can be
-        surpressed by setting $FS::svc_acct::nossh_hack true.
-
-    delete
-        Deletes this account from the database. If there is an
-        error, returns the error, otherwise returns false.
-
-        The corresponding FS::cust_svc record will be deleted as
-        well.
-
-        If the configuration value (see the FS::Conf manpage)
-        shellmachine exists, the command:
-
-          userdel $username
-
-        is executed on shellmachine via ssh. This behaviour can be
-        surpressed by setting $FS::svc_acct::nossh_hack true.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-        If the configuration value (see the FS::Conf manpage)
-        shellmachine exists, and the dir field has changed, the
-        command:
-
-          [ -d $old_dir ] && (
-            chmod u+t $old_dir;
-            umask 022;
-            mkdir $new_dir;
-            cd $old_dir;
-            find . -depth -print | cpio -pdm $new_dir;
-            chmod u-t $new_dir;
-            chown -R $uid.$gid $new_dir;
-            rm -rf $old_dir
-          )
-
-        is executed on shellmachine via ssh. This behaviour can be
-        surpressed by setting $FS::svc_acct::nossh_hack true.
-
-    suspend
-        Suspends this account by prefixing *SUSPENDED* to the
-        password. If there is an error, returns the error, otherwise
-        returns false.
-
-        Called by the suspend method of FS::cust_pkg (see the
-        FS::cust_pkg manpage).
-
-    unsuspend
-        Unsuspends this account by removing *SUSPENDED* from the
-        password. If there is an error, returns the error, otherwise
-        returns false.
-
-        Called by the unsuspend method of FS::cust_pkg (see the
-        FS::cust_pkg manpage).
-
-    cancel
-        Just returns false (no error) for now.
-
-        Called by the cancel method of FS::cust_pkg (see the
-        FS::cust_pkg manpage).
-
-    check
-        Checks all fields to make sure this is a valid service. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-        Sets any fixed values; see the FS::part_svc manpage.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    The remote commands should be configurable.
-
-    The create method should set defaults from part_svc (like the
-    check method sets fixed values).
-
-SEE ALSO
-    the FS::Record manpage, the FS::Conf manpage, the FS::cust_svc
-    manpage, the FS::part_svc manpage, the FS::cust_pkg manpage, the
-    FS::SSH manpage, the ssh manpage, the FS::svc_acct_pop manpage,
-    schema.html from the base documentation.
-
-HISTORY
-    ivan@voicenet.com 97-jul-16 - 21
-
-    rewrite (among other things, now know about part_svc)
-    ivan@sisd.com 98-mar-8
-
-    Changed 'password' to '_password' because Pg6.3 reserves the
-    password word bmccane@maxbaud.net 98-apr-3
-
-    username length and shell no longer hardcoded ivan@sisd.com 98-
-    jun-28
-
-    eww but needed: ignore uid duplicates for 'fax' and 'hylafax'
-    ivan@sisd.com 98-jun-29
-
-    $nossh_hack ivan@sisd.com 98-jul-13
-
-    protections against UID/GID of 0 for incorrectly-setup RDBMSs
-    (also in bin/svc_acct.export) ivan@sisd.com 98-jul-13
-
-    arbitrary radius attributes ivan@sisd.com 98-aug-13
-
-    /var/spool/freeside/conf/shellmachine ivan@sisd.com 98-aug-13
-
-    pod and FS::conf ivan@sisd.com 98-sep-22
-
diff --git a/htdocs/docs/man/svc_acct_pop.txt b/htdocs/docs/man/svc_acct_pop.txt
deleted file mode 100644 (file)
index ac09654..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-NAME
-    FS::svc_acct_pop - Object methods for svc_acct_pop records
-
-SYNOPSIS
-      use FS::svc_acct_pop;
-
-      $record = create FS::svc_acct_pop \%hash;
-      $record = create FS::svc_acct_pop { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::svc_acct object represents an point of presence.
-    FS::svc_acct_pop inherits from FS::Record. The following fields
-    are currently supported:
-
-    popnum - primary key (assigned automatically for new accounts)
-    city
-    state
-    ac - area code
-    exch - exchange
-METHODS
-    create HASHREF
-        Creates a new point of presence (if only it were that
-        easy!). To add the point of presence to the database, see
-        the section on "insert".
-
-    insert
-        Adds this point of presence to the databaes. If there is an
-        error, returns the error, otherwise returns false.
-
-    delete
-        Currently unimplemented.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    check
-        Checks all fields to make sure this is a valid point of
-        presence. If there is an error, returns the error, otherwise
-        returns false. Called by the insert and replace methods.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    It should be renamed to part_pop.
-
-SEE ALSO
-    the FS::Record manpage, the svc_acct manpage, schema.html from
-    the base documentation.
-
-HISTORY
-    Class dealing with pops
-
-    ivan@sisd.com 98-mar-8
-
-    pod ivan@sisd.com 98-sep-23
-
diff --git a/htdocs/docs/man/svc_acct_sm.txt b/htdocs/docs/man/svc_acct_sm.txt
deleted file mode 100644 (file)
index e9940af..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-NAME
-    FS::svc_acct_sm - Object methods for svc_acct_sm records
-
-SYNOPSIS
-      use FS::svc_acct_sm;
-
-      $record = create FS::svc_acct_sm \%hash;
-      $record = create FS::svc_acct_sm { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-      $error = $record->suspend;
-
-      $error = $record->unsuspend;
-
-      $error = $record->cancel;
-
-DESCRIPTION
-    An FS::svc_acct object represents a virtual mail alias.
-    FS::svc_acct inherits from FS::Record. The following fields are
-    currently supported:
-
-    svcnum - primary key (assigned automatcially for new accounts)
-    domsvc - svcnum of the virtual domain (see the FS::svc_domain manpage)
-    domuid - uid of the target account (see the FS::svc_acct manpage)
-    domuser - virtual username
-METHODS
-    create HASHREF
-        Creates a new virtual mail alias. To add the virtual mail
-        alias to the database, see the section on "insert".
-
-    insert
-        Adds this virtual mail alias to the database. If there is an
-        error, returns the error, otherwise returns false.
-
-        The additional fields pkgnum and svcpart (see the
-        FS::cust_svc manpage) should be defined. An FS::cust_svc
-        record will be created and inserted.
-
-        If the configuration values (see the FS::Conf manpage)
-        shellmachine and qmailmachines exist, and domuser is `*'
-        (meaning a catch-all mailbox), the command:
-
-          [ -e $dir/.qmail-$qdomain-default ] || {
-            touch $dir/.qmail-$qdomain-default;
-            chown $uid:$gid $dir/.qmail-$qdomain-default;
-          }
-
-        is executed on shellmachine via ssh (see the section on
-        "EXTENSION ADDRESSES" in the dot-qmail manpage). This
-        behaviour can be surpressed by setting
-        $FS::svc_acct_sm::nossh_hack true.
-
-    delete
-        Deletes this virtual mail alias from the database. If there
-        is an error, returns the error, otherwise returns false.
-
-        The corresponding FS::cust_svc record will be deleted as
-        well.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    suspend
-        Just returns false (no error) for now.
-
-        Called by the suspend method of FS::cust_pkg (see the
-        FS::cust_pkg manpage).
-
-    unsuspend
-        Just returns false (no error) for now.
-
-        Called by the unsuspend method of FS::cust_pkg (see the
-        FS::cust_pkg manpage).
-
-    cancel
-        Just returns false (no error) for now.
-
-        Called by the cancel method of FS::cust_pkg (see the
-        FS::cust_pkg manpage).
-
-    check
-        Checks all fields to make sure this is a valid virtual mail
-        alias. If there is an error, returns the error, otherwise
-        returns false. Called by the insert and replace methods.
-
-        Sets any fixed values; see the FS::part_svc manpage.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    The remote commands should be configurable.
-
-SEE ALSO
-    the FS::Record manpage, the FS::Conf manpage, the FS::cust_svc
-    manpage, the FS::part_svc manpage, the FS::cust_pkg manpage, the
-    FS::svc_acct manpage, the FS::svc_domain manpage, the FS::SSH
-    manpage, the ssh manpage, the dot-qmail manpage, schema.html
-    from the base documentation.
-
-HISTORY
-    ivan@voicenet.com 97-jul-16 - 21
-
-    rewrite ivan@sisd.com 98-mar-10
-
-    s/qsearchs/qsearch/ to eliminate warning ivan@sisd.com 98-apr-19
-
-    uses conf/shellmachine and has an nossh_hack ivan@sisd.com 98-
-    jul-14
-
-    s/\./:/g in .qmail-domain:com ivan@sisd.com 98-aug-13
-
-    pod, FS::Conf, moved .qmail file from check to insert 98-sep-23
-
diff --git a/htdocs/docs/man/svc_domain.txt b/htdocs/docs/man/svc_domain.txt
deleted file mode 100644 (file)
index 03d3dbc..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-NAME
-    FS::svc_domain - Object methods for svc_domain records
-
-SYNOPSIS
-      use FS::svc_domain;
-
-      $record = create FS::svc_domain \%hash;
-      $record = create FS::svc_domain { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-      $error = $record->suspend;
-
-      $error = $record->unsuspend;
-
-      $error = $record->cancel;
-
-DESCRIPTION
-    An FS::svc_domain object represents a domain. FS::svc_domain
-    inherits from FS::Record. The following fields are currently
-    supported:
-
-    svcnum - primary key (assigned automatically for new accounts)
-    domain
-METHODS
-    create HASHREF
-        Creates a new domain. To add the domain to the database, see
-        the section on "insert".
-
-    insert
-        Adds this domain to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-        The additional fields *pkgnum* and *svcpart* (see the
-        FS::cust_svc manpage) should be defined. An FS::cust_svc
-        record will be created and inserted.
-
-        The additional field *action* should be set to *N* for new
-        domains or *M* for transfers.
-
-        A registration or transfer email will be submitted unless
-        $FS::svc_domain::whois_hack is true.
-
-    delete
-        Deletes this domain from the database. If there is an error,
-        returns the error, otherwise returns false.
-
-        The corresponding FS::cust_svc record will be deleted as
-        well.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    suspend
-        Just returns false (no error) for now.
-
-        Called by the suspend method of FS::cust_pkg (see the
-        FS::cust_pkg manpage).
-
-    unsuspend
-        Just returns false (no error) for now.
-
-        Called by the unsuspend method of FS::cust_pkg (see the
-        FS::cust_pkg manpage).
-
-    cancel
-        Just returns false (no error) for now.
-
-        Called by the cancel method of FS::cust_pkg (see the
-        FS::cust_pkg manpage).
-
-    check
-        Checks all fields to make sure this is a valid domain. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-        Sets any fixed values; see the FS::part_svc manpage.
-
-    _whois
-        Executes the command:
-
-          whois do $domain
-
-        and returns the output.
-
-        (Always returns *No match for domian "$domain".* if
-        $FS::svc_domain::whois_hack is set true.)
-
-    submit_internic
-        Submits a registration email for this domain.
-
-BUGS
-    It doesn't properly override FS::Record yet.
-
-    All BIND/DNS fields should be included (and exported).
-
-    All registries should be supported.
-
-    Not all configuration access is through FS::Conf!
-
-    Should change action to a real field.
-
-SEE ALSO
-    the FS::Record manpage, the FS::Conf manpage, the FS::cust_svc
-    manpage, the FS::part_svc manpage, the FS::cust_pkg manpage, the
-    FS::SSH manpage, the ssh manpage, the dot-qmail manpage,
-    schema.html from the base documentation, config.html from the
-    base documentation.
-
-HISTORY
-    ivan@voicenet.com 97-jul-21
-
-    rewrite ivan@sisd.com 98-mar-10
-
-    add internic bits ivan@sisd.com 98-mar-14
-
-    Changed 'day' to 'daytime' because Pg6.3 reserves the day word
-    bmccane@maxbaud.net 98-apr-3
-
-    /var/spool/freeside/conf/registries/internic/, Mail::Internet,
-    etc. ivan@sisd.com 98-jul-17-19
-
-    pod, some FS::Conf (not complete) ivan@sisd.com 98-sep-23
-
diff --git a/htdocs/docs/man/type_pkgs.txt b/htdocs/docs/man/type_pkgs.txt
deleted file mode 100644 (file)
index 9822b48..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-NAME
-    FS::type_pkgs - Object methods for type_pkgs records
-
-SYNOPSIS
-      use FS::type_pkgs;
-
-      $record = create FS::type_pkgs \%hash;
-      $record = create FS::type_pkgs { 'column' => 'value' };
-
-      $error = $record->insert;
-
-      $error = $new_record->replace($old_record);
-
-      $error = $record->delete;
-
-      $error = $record->check;
-
-DESCRIPTION
-    An FS::type_pkgs record links an agent type (see the
-    FS::agent_type manpage) to a billing item definition (see the
-    FS::part_pkg manpage). FS::type_pkgs inherits from FS::Record.
-    The following fields are currently supported:
-
-    typenum - Agent type, see the FS::agent_type manpage
-    pkgpart - Billing item definition, see the FS::part_pkg manpage
-METHODS
-    create HASHREF
-        Create a new record. To add the record to the database, see
-        the section on "insert".
-
-    insert
-        Adds this record to the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    delete
-        Deletes this record from the database. If there is an error,
-        returns the error, otherwise returns false.
-
-    replace OLD_RECORD
-        Replaces OLD_RECORD with this one in the database. If there
-        is an error, returns the error, otherwise returns false.
-
-    check
-        Checks all fields to make sure this is a valid record. If
-        there is an error, returns the error, otherwise returns
-        false. Called by the insert and replace methods.
-
-HISTORY
-    Defines the relation between agent types and pkgparts (Which
-    pkgparts can the different [types of] agents sell?)
-
-    ivan@sisd.com 97-nov-13
-
-    change to ut_ FS::Record, fixed bugs ivan@sisd.com 97-dec-10
-
diff --git a/htdocs/docs/passwd.html b/htdocs/docs/passwd.html
deleted file mode 100644 (file)
index a8f8151..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-<head>
-  <title>fs_passwd</title>
-</head>
-<body>
-  <h1>fs_passwd</h1>
-You may use fs_passwd/fs_passwd as a "passwd", "chfn" and "chsh" replacement on your shell machine(s) to cause password, gecos and shell changes to update your freeside machine.  This can pose a security risk if not configured correctly.  <b>Do not use this feature unless you understand what you are doing!</b>
-<br><br>Currently it is assumed that the the crypt(3) function in the C library is the same on the Freeside machine as on the target machine.
-<ul>
-  <li>Create a freeside account on the shell machine(s).
-  <li>Append the identity.pub from the freeside user on your freeside machine to the authorized_keys file of the newly created freeside user on the shell machine(s).
-  <li>Copy fs_passwd/fs_passwd to /usr/local/bin on the shell machine(s).  (chown freeside, chmod 4755).  You may link it to passwd, chfn and chsh as well.
-  <li>Copy fs_passwd/fs_passwdd to /usr/local/sbin on the shell machine(s).  (chown freeside, chmod 500)
-  <li>Create /usr/local/freeside on the shell machine(s). (chown freeside, chmod 700)
-  <li>Run an iteration of "fs_passwd/fs_passwd_server shell.machine" as the freeside user for each shell machine (this is a daemon process).
-</ul>
-</body>
diff --git a/htdocs/docs/schema.html b/htdocs/docs/schema.html
deleted file mode 100644 (file)
index 5a296ec..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-<head>
-  <title>Schema reference</title>
-</head>
-<body>
-  <h1>Schema reference</h1>
-  <ul>
-    <li><a name="agent">agent</a> - Agents are resellers of your service.  Agents may be limited to a subset of your full offerings (via their agent type).
-      <ul>
-        <li>agentnum - primary key
-        <li>agent - name of this agent
-        <li>typenum - <a href="#agent_type">agent type</a>
-        <li>prog - (unimplemented)
-        <li>freq - (unimplemented)
-      </ul>
-    <li><a name="agent_type">agent_type</a> - Agent types define groups of packages that you can then assign to particular agents.
-      <ul>
-        <li>typenum - primary key
-        <li>atype - name of this agent type
-      </ul>
-    <li><a name="cust_bill">cust_bill</a> - Invoices
-      <ul>
-        <li>invnum - primary key
-        <li>custnum - <a href="#cust_main">customer</a>
-        <li>_date
-        <li>charged - amount of this invoice
-        <li>owed - amount still outstanding on this invoice
-        <li>printed - how many times this invoice has been printed automatically
-      </ul>
-    <li><a name="cust_bill_pkg">cust_bill_pkg</a> - Invoice line items
-      <ul>
-        <li>invnum - (multiple) key
-        <li>pkgnum - <a href="#cust_pkg">package</a>
-        <li>setup - setup fee 
-        <li>recur - recurring fee
-        <li>sdate - starting date
-        <li>edate - ending date
-      </ul>
-    <li><a name="cust_credit">cust_credit</a> - Credits
-      <ul>
-        <li>crednum - primary key
-        <li>custnum - <a href="#cust_main">customer</a>
-        <li>amount - amount credited
-        <li>credited - amount still outstanding (not yet refunded) on this credit
-        <li>_date
-        <li>otaker - order taker
-        <li>reason
-      </ul>
-    <li><a name="cust_main">cust_main</a> - Customers
-      <ul>
-        <li>custnum - primary key
-        <li>agentnum - <a href="#agent">agent</a>
-        <li>refnum - <a href="#part_referral">referral</a>
-        <li>first - name
-        <li>last - name
-        <li>ss - social security number
-        <li>company
-        <li>address1
-        <li>address2
-        <li>city
-        <li>county
-        <li>state
-        <li>zip
-        <li>country
-        <li>daytime - phone
-        <li>night - phone
-        <li>payby - CARD, BILL, or COMP
-        <li>payinfo - card number, P.O.#, or comp issuer
-        <li>paydate - expiration date
-        <li>payname - billing name (name on card)
-        <li>tax - tax exempt, Y or null
-        <li>otaker - order taker
-      </ul>
-    <li><a name="cust_main_county">cust_main_county</a> - Tax rates
-      <ul>
-        <li>taxnum - primary key
-        <li>state
-        <li>county
-        <li>tax - % rate
-      </ul>
-    <li><a name="cust_pay">cust_pay</a> - Payments
-      <ul>
-        <li>paynum - primary key
-        <li>invnum - <a href="#cust_bill">invoice</a>
-        <li>paid - amount
-        <li>_date
-        <li>payby - CARD, BILL, or COMP
-        <li>payinfo - card number, P.O.#, or comp issuer
-        <li>paybatch - text field for tracking card processor batches
-      </ul>
-    <li><a name="cust_pay_batch">cust_pay_batch</a> - Pending batch
-      <ul>
-        <li>trancode - 77 for charges
-        <li>cardnum
-        <li>exp - card expiration
-        <li>amount
-        <li>invnum - <a href="#cust_bill">invoice</a>
-        <li>custnum - <a href="#cust_main">customer</a>
-        <li>payname - name on card
-        <li>first - name
-        <li>last - name
-        <li>address1
-        <li>address2
-        <li>city
-        <li>state
-        <li>zip
-        <li>country
-      </ul>
-    <li><a name="cust_pkg">cust_pkg</a> - Customer billing items
-      <ul>
-        <li>pkgnum - primary key
-        <li>custnum - <a href="#cust_main">customer</a>
-        <li>pkgpart - <a href="#part_pkg">Package definition</a>
-        <li>setup - date
-        <li>bill - next bill date
-        <li>susp - (past) suspension date
-        <li>expire - (future) cancellation date
-        <li>cancel - (past) cancellation date
-        <li>otaker - order taker
-      </ul>
-    <li><a name="cust_refund">cust_refund</a> - Refunds
-      <ul>
-        <li>refundnum - primary key
-        <li>crednum - <a href="#cust_credit">credit</a>
-        <li>refund - amount
-        <li>_date
-        <li>payby - CARD, BILL or COMP
-        <li>payinfo - card number, P.O.#, or comp issuer
-        <li>otaker - order taker
-      </ul>
-    <li><a name="cust_svc">cust_svc</a> - Customer services
-      <ul>
-        <li>svcnum - primary key
-        <li>pkgnum - <a href="#cust_pkg">package</a>
-        <li>svcpart - <a href="#part_svc">Service definition</a>
-      </ul>
-    <li><a name="part_pkg">part_pkg</a> - Package definitions
-      <ul>
-        <li>pkgpart - primary key
-        <li>pkg - package name
-        <li>comment - non-customer visable package comment
-        <li>setup - setup fee
-        <li>freq - recurring frequency (months)
-        <li>recur - recurring fee
-      </ul>
-    <li><a name="part_referral">part_referral</a> - Referral listing
-      <ul>
-        <li>refnum</li> - primary key
-        <li>referral</li> - referral
-      </ul>
-    <li><a name="part_svc">part_svc</a> - Service definitions
-      <ul>
-        <li>svcpart - primary key
-        <li>svc - name of this service
-        <li>svcdb - table used for this service: svc_acct, svc_acct_sm, svc_domain, svc_charge or svc_wo
-        <li><i>table</i>__<i>field</i> - Default or fixed value for <i>field</i> in <i>table</i>
-        <li><i>table</i>__<i>field</i>_flag - null, D or F
-      </ul>
-    <li><a name="pkg_svc">pkg_svc</a>
-      <ul>
-        <li>pkgpart - <a href="#part_pkg">Package definition</a>
-        <li>svcpart - <a href="#part_svc">Service definition</a>
-        <li>quantity - quantity of this service that this package includes
-      </ul>
-    <li><a name="svc_acct">svc_acct</a> - Accounts
-      <ul>
-        <li>svcnum - <a href="#cust_svc">primary key</a>
-        <li>username
-        <li>_password
-        <li>popnum - <a href="#svc_acct_pop">Point of Presence</a>
-        <li>uid
-        <li>gid
-        <li>finger - GECOS
-        <li>dir
-        <li>shell
-        <li>quota - (unimplementd)
-        <li>slipip - IP address
-        <li>radius_<i>Radius_Attribute</i> - Radius-Attribute
-      </ul>
-    <li><a name="svc_acct_pop">svc_acct_pop</a> - Points of Presence
-      <ul>
-        <li>popnum - primary key
-        <li>city
-        <li>state
-        <li>ac - area code
-        <li>exch - exchange
-      </ul>
-    <li><a name="svc_acct_sm">svc_acct_sm</a> - Domain mail aliases
-      <ul>
-        <li>svcnum - <a href="#cust_svc">primary key</a>
-        <li>domsvc - <a href="#svc_domain">Domain</a> (by svcnum)
-        <li>domuid - <a href="#svc_acct">Account</a> (by uid)
-        <li>domuser - domuser @ <a href="#svc_domain">Domain</a> forwards to <a href="#svc_acct">Account</a>
-      </ul>
-    <li><a name="svc_domain">svc_domain</a> - Domains
-      <ul>
-        <li>svcnum - <a href="#cust_svc">primary key</a>
-        <li>domain
-      </ul>
-    <li><a name="type_pkgs">type_pkgs</a>
-      <ul>
-        <li>typenum - <a href="#agent_type">agent type</a>
-        <li>pkgpart - <a href="#part_pkg">Package definition</a>
-      </ul>
-  </ul>
-</body>
diff --git a/htdocs/docs/trouble.html b/htdocs/docs/trouble.html
deleted file mode 100644 (file)
index 2cf6d4e..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<head>
-  <title>Troubleshooting</title>
-</head>
-<body>
-  <h1>Troubleshooting</h1>
-  <ul>
-    <li>When troubleshooting the web interface, helpful information is often in your web server's error log.
-    <li>Internet Explorer will not work with Freeside's HTML interface. 
-<a HREF="http://www.netscape.com">Netscape</a>,                                 
-<a HREF="http://lynx.browser.org">Lynx</a>, and                                 
-<a HREF="http://www.cs.indiana.edu/elisp/w3/docs.html">Emacs/W3</a>,            
-among others, should work fine.
-    <li>If bin/svc_acct.import fails with an "Out of memory!" error using MySQL, upgrede MySQL and recompile the Perl DBD.  There was a memory leak in some older versions of MySQL.
-    <li>If you get tons of errors in your web server's error log like this:
-<pre>
-Ambiguous use of value => resolved to "value" =>
-at /usr/lib/perl5/site_perl/File/CounterFile.pm line 132.
-</pre>
-        This clutters up your log files but is otherwise harmless.  Upgrade to the latest File::CounterFile. 
-    <li>If you get an Internal Server Error when adding or editing, but find that the update has occured, and you get something like the following in your web server's error log:
-<pre>
-access to <i>/your/path</i>/edit/process/<i>some_table</i>.cgi failed for
-<i>machine.domain.tld</i>, reason: malformed header from script.
-Bad header=HTTP/1.0 302 Moved Temporarily
-</pre>
-        Then you forgot to apply this <a href="CGI-modules-2.76-patch.txt">patch</a> as mentioned in the <a href="install.html">New Installation</a> section of the documentation.
-    <li>If you get errors like this:
-<pre>
-UID.pm: Can't open /var/spool/freeside/conf/secrets: Permission denied 
-at <i>/your/path</i>/site_perl/FS/UID.pm line 26.
-BEGIN failed--compilation aborted at
-<i>/your/path</i>/edit/process/part_svc.cgi line 15.
-</pre>
-        Then the scripts are not running setuid freeside.  If you were editing
-the files, it is possible you inadvertantly removed the setuid bit.
-As mentioned in the <a href="install.html">New Installation</a> section of the documentation, set ownership and permissions for the web interface.  Your system should support secure setuid scripts or Perl's emulation, see <a href="http://www.perl.com/CPAN-local/doc/manual/html/pod/perlsec.html#Security_Bugs">perlsec: Security Bugs</a> for information and workarounds.
-<pre>cd /usr/local/apache/htdocs/freeside
-chown -R freeside .
-chmod 4755 browse/*.cgi edit/*.cgi edit/process/*.cgi misc/*.cgi misc/process/*.cgi search/*.cgi view/*.cgi</pre>
-  </ul>
-</body>
diff --git a/htdocs/docs/upgrade.html b/htdocs/docs/upgrade.html
deleted file mode 100644 (file)
index d2201f6..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<head>
-  <title>Upgrading to 1.1.x</title>
-</head>
-<body>
-<h1>Upgrading to 1.1.x</h1>
-<ul>
-  <li>Back up your data and current Freeside installation.
-  <li>Unpack a copy of the 1.0.0 distribution in a separate location.
-  <li>Diff your current installation against the 1.0.0 distribution.
-  <li>Apply all the diffs you found above, if applicable.
-  <li>Apply (at least) the following changes to your database:
-<pre>
-ALTER TABLE cust_main CHANGE ss ss char(11) NULL;
-ALTER TABLE cust_main CHANGE day daytime varchar(20) NULL;
-ALTER TABLE svc_acct CHANGE password _password varchar(25) NOT NULL;
-ALTER TABLE part_svc CHANGE svc_acct__password svc_acct___password varchar(25) NULL;
-ALTER TABLE part_svc CHANGE svc_acct__password_flag svc_acct___password_flag char(1) NULL;
-ALTER TABLE agent_type CHANGE type atype varchar(80) NOT NULL;
-</pre>
-  <li>Optionally change the field lengths and types to match a 1.1.x install; see `bin/fs-setup'.
-  <li>Create the necessary <a href="config.html">configuration files</a>,
-  <li>Copy or symlink htdocs and site_perl to the new 1.1.x copies.
-  <li>Run bin/dbdef-create.  This file uses MySQL-specific syntax.  If you are running a different database engine you will need to modify it slightly.
-</body>
diff --git a/htdocs/docs/upgrade2.html b/htdocs/docs/upgrade2.html
deleted file mode 100644 (file)
index 4bf7ea4..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<head>
-  <title>Upgrading to 1.1.3</title>
-</head>
-<body>
-<h1>Upgrading to 1.1.3 from 1.1.x</h1>
-<ul>
-  <li>If migrating from 1.0.0, see these <a href="upgrade.html">instructions</a> first.
-  <li>Back up your data and current Freeside installation.
-  <li>If applicable, create the new <a href="config.html">configuration files</a>: lpr, cybercash2, cybercash3.2
-  <li>Copy or symlink htdocs and site_perl to the new copies.
-</body>
diff --git a/htdocs/edit/agent.cgi b/htdocs/edit/agent.cgi
deleted file mode 100755 (executable)
index 5bd1165..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# agent.cgi: Add/Edit agent (output form)
-#
-# ivan@sisd.com 97-dec-12
-#
-# Changes to allow page to work at a relative position in server
-# Changed 'type' to 'atype' because Pg6.3 reserves the type word
-#      bmccane@maxbaud.net     98-apr-3
-#
-# use FS::CGI, added inline documentation ivan@sisd.com 98-jul-12
-
-use strict;
-use CGI::Base;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::agent;
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-my($agent,$action);
-if ( $cgi->var('QUERY_STRING') =~ /^(\d+)$/ ) { #editing
-  $agent=qsearchs('agent',{'agentnum'=>$1});
-  $action='Edit';
-} else { #adding
-  $agent=create FS::agent {};
-  $action='Add';
-}
-my($hashref)=$agent->hashref;
-
-print header("$action Agent", menubar(
-  'Main Menu' => '../',
-  'View all agents' => '../browse/agent.cgi',
-)), '<FORM ACTION="process/agent.cgi" METHOD=POST>';
-
-print qq!<INPUT TYPE="hidden" NAME="agentnum" VALUE="$hashref->{agentnum}">!,
-      "Agent #", $hashref->{agentnum} ? $hashref->{agentnum} : "(NEW)";
-
-print <<END;
-<PRE>
-Agent                     <INPUT TYPE="text" NAME="agent" SIZE=32 VALUE="$hashref->{agent}">
-Agent type                <SELECT NAME="typenum" SIZE=1>
-END
-
-my($agent_type);
-foreach $agent_type (qsearch('agent_type',{})) {
-  print "<OPTION";
-  print " SELECTED"
-    if $hashref->{typenum} == $agent_type->getfield('typenum');
-  print ">", $agent_type->getfield('typenum'), ": ",
-        $agent_type->getfield('atype'),"\n";
-}
-
-print <<END;
-</SELECT>
-Frequency (unimplemented) <INPUT TYPE="text" NAME="freq" VALUE="$hashref->{freq}">
-Program (unimplemented)   <INPUT TYPE="text" NAME="prog" VALUE="$hashref->{prog}">
-</PRE>
-END
-
-print qq!<BR><INPUT TYPE="submit" VALUE="!,
-      $hashref->{agentnum} ? "Apply changes" : "Add agent",
-      qq!">!;
-
-print <<END;
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/agent_type.cgi b/htdocs/edit/agent_type.cgi
deleted file mode 100755 (executable)
index b9fff45..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# agent_type.cgi: Add/Edit agent type (output form)
-#
-# ivan@sisd.com 97-dec-10
-#
-# Changes to allow page to work at a relative position in server
-# Changed 'type' to 'atype' because Pg6.3 reserves the type word
-#      bmccane@maxbaud.net     98-apr-3
-#
-# use FS::CGI, added inline documentation ivan@sisd.com 98-jul-12
-
-use strict;
-use CGI::Base;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::agent_type;
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-my($agent_type,$action);
-if ( $cgi->var('QUERY_STRING') =~ /^(\d+)$/ ) { #editing
-  $agent_type=qsearchs('agent_type',{'typenum'=>$1});
-  $action='Edit';
-} else { #adding
-  $agent_type=create FS::agent_type {};
-  $action='Add';
-}
-my($hashref)=$agent_type->hashref;
-
-print header("$action Agent Type", menubar(
-  'Main Menu' => '../',
-  'View all agent types' => '../browse/agent_type.cgi',
-)), '<FORM ACTION="process/agent_type.cgi" METHOD=POST>';
-
-print qq!<INPUT TYPE="hidden" NAME="typenum" VALUE="$hashref->{typenum}">!,
-      "Agent Type #", $hashref->{typenum} ? $hashref->{typenum} : "(NEW)";
-
-print <<END;
-<BR>Type <INPUT TYPE="text" NAME="atype" SIZE=32 VALUE="$hashref->{atype}">
-<P>Select which packages agents of this type may sell to customers</P>
-END
-
-my($part_pkg);
-foreach $part_pkg ( qsearch('part_pkg',{}) ) {
-  print qq!<BR><INPUT TYPE="checkbox" NAME="pkgpart!,
-        $part_pkg->getfield('pkgpart'), qq!" !,
-       # ( 'CHECKED 'x scalar(
-        qsearchs('type_pkgs',{
-          'typenum' => $agent_type->getfield('typenum'),
-          'pkgpart'  => $part_pkg->getfield('pkgpart'),
-        })
-          ? 'CHECKED '
-          : '',
-        qq!"VALUE="ON"> !,$part_pkg->getfield('pkg')
-  ;
-}
-
-print qq!<BR><INPUT TYPE="submit" VALUE="!,
-      $hashref->{typenum} ? "Apply changes" : "Add agent type",
-      qq!">!;
-
-print <<END;
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/cust_credit.cgi b/htdocs/edit/cust_credit.cgi
deleted file mode 100755 (executable)
index 75ef212..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_credit.cgi: Add a credit (output form)
-#
-# Usage: cust_credit.cgi custnum [ -paybatch ]
-#        http://server.name/path/cust_credit?custnum [ -paybatch ]
-#
-# Note: Should be run setuid root as user nobody.
-#
-# some hooks in here for modifications as well as additions, but needs (lots) more work.
-# also see process/cust_credit.cgi, the script that processes the form.
-#
-# ivan@voicenet.com 96-dec-05
-#
-# paybatch field, differentiates between credits & credits+refunds by commandline
-# ivan@voicenet.com 96-dec-08
-#
-# added (but commented out) sprintf("%.2f" in amount field.  Hmm.
-# ivan@voicenet.com 97-jan-3
-#
-# paybatch stuff thrown out - has checkbox now instead.  
-# (well, sort of.  still passed around for backward compatability and possible editing hook)
-# ivan@voicenet.com 97-apr-21
-#
-# rewrite ivan@sisd.com 98-mar-16
-
-use strict;
-use Date::Format;
-use CGI::Base qw(:DEFAULT :CGI); #CGI module
-use FS::UID qw(cgisuidsetup getotaker);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-cgisuidsetup($cgi);
-
-#untaint custnum
-$QUERY_STRING =~ /^(\d+)$/;
-my($custnum)=$1;
-
-#untaint otaker
-my($otaker)=getotaker;
-
-SendHeaders(); # one guess.
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Post Credit</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Post Credit</H1>
-    </CENTER>
-    <FORM ACTION="process/cust_credit.cgi" METHOD=POST>
-    <HR><PRE>
-END
-
-#crednum
-my($crednum)="";
-print qq!Credit #<B>!, $crednum ? $crednum : " <I>(NEW)</I>", qq!</B><INPUT TYPE="hidden" NAME="crednum" VALUE="$crednum">!;
-
-#custnum
-print qq!\nCustomer #<B>$custnum</B><INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!;
-
-#paybatch
-print qq!<INPUT TYPE="hidden" NAME="paybatch" VALUE="">!;
-
-#date
-my($date)=time;
-print qq!\nDate: <B>!, time2str("%D",$date), qq!</B><INPUT TYPE="hidden" NAME="_date" VALUE="$date">!;
-
-#amount
-my($amount)='';
-print qq!\nAmount \$<INPUT TYPE="text" NAME="amount" VALUE="$amount" SIZE=8 MAXLENGTH=8>!;
-
-#refund?
-#print qq! <INPUT TYPE="checkbox" NAME="refund" VALUE="yes">Also post refund!;
-
-#otaker (hidden)
-print qq!<INPUT TYPE="hidden" NAME="otaker" VALUE="$otaker">!;
-
-#reason
-my($reason)='';
-print qq!\nReason <INPUT TYPE="text" NAME="reason" VALUE="$reason" SIZE=72>!;
-
-print <<END;
-</PRE>
-<BR>
-<CENTER><INPUT TYPE="submit" VALUE="Post"></CENTER>
-END
-
-print <<END;
-
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/cust_main.cgi b/htdocs/edit/cust_main.cgi
deleted file mode 100755 (executable)
index 1455601..0000000
+++ /dev/null
@@ -1,214 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_main.cgi: Edit a customer (output form)
-#
-# Usage: cust_main.cgi custnum
-#        http://server.name/path/cust_main.cgi?custnum
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# ivan@voicenet.com 96-nov-29 -> 96-dec-04
-#
-# Blank custnum for new customer.
-# ivan@voicenet.com 96-dec-16
-#
-# referral defaults to blank, to force people to pick something
-# ivan@voicenet.com 97-jun-4
-#
-# rewrote for new API
-# ivan@voicenet.com 97-jul-28
-#
-# new customer is null, not '#'
-# otaker gotten from &getotaker instead of $ENV{REMOTE_USER}
-# ivan@sisd.com 97-nov-12
-#
-# cgisuidsetup($cgi);
-# no need for old_ fields.
-# now state+county is a select field (took out PA hack)
-# used autoloaded $cust_main->field methods
-# ivan@sisd.com 97-dec-17
-#
-# fixed quoting problems ivan@sisd.com 98-feb-23
-#
-# paydate sql update ivan@sisd.com 98-mar-5
-#
-# Changes to allow page to work at a relative position in server
-# Changed 'day' to 'daytime' because Pg6.3 reserves the day word
-# Added test for paydate in mm-dd-yyyy format for Pg6.3 default format
-#      bmccane@maxbaud.net     98-apr-3
-#
-# fixed one missed day->daytime ivan@sisd.com 98-jul-13
-
-use strict;
-use CGI::Base;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup getotaker);
-use FS::Record qw(qsearch qsearchs);
-use FS::cust_main;
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-#get record
-my($custnum,$action,$cust_main);
-if ( $cgi->var('QUERY_STRING') =~ /^(\d+)$/ ) { #editing
-  $custnum=$1;
-  $cust_main = qsearchs('cust_main',{'custnum'=>$custnum});
-  $action='Edit';
-} else {
-  $custnum='';
-  $cust_main = create FS::cust_main ( {} );
-  $cust_main->setfield('otaker',&getotaker);
-  $cust_main->setfield('country','US');
-  $action='Add';
-}
-
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Customer $action</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Customer $action</H1>
-    </CENTER>
-    <FORM ACTION="process/cust_main.cgi" METHOD=POST>
-    <PRE>
-END
-
-print qq!<INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!,
-      qq!Customer #<FONT SIZE="+1"><B>!;
-print $custnum ? $custnum : " (NEW)" , "</B></FONT>";
-
-#agentnum
-my($agentnum)=$cust_main->agentnum || 1; #set to first agent by default
-my(@agents) = qsearch('agent',{});
-print qq!\n\nAgent # <SELECT NAME="agentnum" SIZE="1">!;
-my($agent);
-foreach $agent (sort {
-  $a->agent cmp $b->agent;
-} @agents) {
-    print "<OPTION" . " SELECTED"x($agent->agentnum==$agentnum),
-    ">", $agent->agentnum,": ", $agent->agent, "\n";
-}
-print "</SELECT>";
-
-#referral
-#unless ($custnum) {
-  my($refnum)=$cust_main->refnum || 0; #to avoid "arguement not numeric" error
-  my(@referrals) = qsearch('part_referral',{});
-  print qq!\nReferral <SELECT NAME="refnum" SIZE="1">!;
-  print "<OPTION> \n";
-  my($referral);
-  foreach $referral (sort {
-    $a->refnum <=> $b->refnum;
-  } @referrals) {
-    print "<OPTION" . " SELECTED"x($referral->refnum==$refnum),
-    ">", $referral->refnum, ": ", $referral->referral,"\n";
-  }
-  print "</SELECT>";
-#}
-
-my($last,$first,$ss,$company,$address1,$address2,$city)=(
-  $cust_main->last,
-  $cust_main->first,
-  $cust_main->ss,
-  $cust_main->company,
-  $cust_main->address1,
-  $cust_main->address2,
-  $cust_main->city,
-);
-
-print <<END;
-
-
-Name (last)<INPUT TYPE="text" NAME="last" VALUE="$last"> (first)<INPUT TYPE="text" NAME="first" VALUE="$first">  SS# <INPUT TYPE="text" NAME="ss" VALUE="$ss" SIZE=11 MAXLENGTH=11>
-Company <INPUT TYPE="text" NAME="company" VALUE="$company">
-Address <INPUT TYPE="text" NAME="address1" VALUE="$address1" SIZE=40 MAXLENGTH=40>
-        <INPUT TYPE="text" NAME="address2" VALUE="$address2" SIZE=40 MAXLENGTH=40>
-City <INPUT TYPE="text" NAME="city" VALUE="$city">  State (county) <SELECT NAME="state" SIZE="1">
-END
-
-foreach ( qsearch('cust_main_county',{}) ) {
-  print "<OPTION";
-  print " SELECTED" if ( $cust_main->state eq $_->state
-                      && $cust_main->county eq $_->county );
-  print ">",$_->state;
-  print " (",$_->county,")" if $_->county;
-}
-print "</SELECT>";
-
-my($zip,$country,$daytime,$night,$fax)=(
-  $cust_main->zip,
-  $cust_main->country,
-  $cust_main->daytime,
-  $cust_main->night,
-  $cust_main->fax,
-);
-
-print <<END;
-  Zip <INPUT TYPE="text" NAME="zip" VALUE="$zip" SIZE=10 MAXLENGTH=10>
-Country: <FONT SIZE="+1"><B>$country</B></FONT><INPUT TYPE="hidden" NAME="country" VALUE="$country">
-
-Phone (daytime)<INPUT TYPE="text" NAME="daytime" VALUE="$daytime" SIZE=18 MAXLENGTH=20>  (night)<INPUT TYPE="text" NAME="night" VALUE="$night" SIZE=18 MAXLENGTH=20>  (fax)<INPUT TYPE="text" NAME="fax" VALUE="$fax" SIZE=12 MAXLENGTH=12>
-
-END
-
-my(%payby)=(
-  'CARD' => "Credit card    ",
-  'BILL' => "Billing    ",
-  'COMP' => "Complimentary",
-);
-for (qw(CARD BILL COMP)) {
-  print qq!<INPUT TYPE="radio" NAME="payby" VALUE="$_"!;
-  print qq! CHECKED! if ($cust_main->payby eq "$_");
-  print qq!>$payby{$_}!;
-}
-
-
-my($payinfo,$payname,$otaker)=(
-  $cust_main->payinfo,
-  $cust_main->payname,
-  $cust_main->otaker,
-);
-
-my($paydate);
-if ( $cust_main->paydate =~ /^(\d{4})-(\d{2})-\d{2}$/ ) {
-  $paydate="$2/$1"
-} elsif ( $cust_main->paydate =~ /^(\d{2})-\d{2}-(\d{4}$)/ ) {
-  $paydate="$1/$2"
-}
-else {
-  $paydate='';
-}
-
-print <<END;
-
-  Card number ,   P.O. #   or   Authorization    <INPUT TYPE="text" NAME="payinfo" VALUE="$payinfo" SIZE=19 MAXLENGTH=19>
-END
-
-print qq!Exp. date (MM/YY or MM/YYYY)<INPUT TYPE="text" NAME="paydate" VALUE="$paydate" SIZE=8 MAXLENGTH=7>    Billing name <INPUT TYPE="text" NAME="payname" VALUE="$payname">\n<INPUT TYPE="checkbox" NAME="tax" VALUE="Y"!;
-print qq! CHECKED! if $cust_main->tax eq "Y";
-print qq!> Tax Exempt!;
-
-print <<END;
-
-
-Order taken by: <FONT SIZE="+1"><B>$otaker</B></FONT><INPUT TYPE="hidden" NAME="otaker" VALUE="$otaker">
-</PRE>
-END
-
-print qq!<CENTER><INPUT TYPE="submit" VALUE="!,
-      $custnum ?  "Apply Changes" : "Add Customer", qq!"></CENTER>!;
-
-print <<END;
-
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/cust_main_county-expand.cgi b/htdocs/edit/cust_main_county-expand.cgi
deleted file mode 100755 (executable)
index 59ff704..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_main_county-expand.cgi: Expand a state into counties (output form)
-#
-# ivan@sisd.com 97-dec-16
-#
-# Changes to allow page to work at a relative position in server
-#      bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-$cgi->var('QUERY_STRING') =~ /^(\d+)$/
-  or die "Illegal taxnum!";
-my($taxnum)=$1;
-
-my($cust_main_county)=qsearchs('cust_main_county',{'taxnum'=>$taxnum});
-die "Can't expand entry!" if $cust_main_county->getfield('county');
-
-print header("Tax Rate (expand state)", menubar(
-  'Main Menu' => '../',
-)), <<END;
-    <FORM ACTION="process/cust_main_county-expand.cgi" METHOD=POST>
-      <INPUT TYPE="hidden" NAME="taxnum" VALUE="$taxnum">
-      Separate counties by
-      <INPUT TYPE="radio" NAME="delim" VALUE="n" CHECKED>line
-      (rumor has it broken on some browsers) or
-      <INPUT TYPE="radio" NAME="delim" VALUE="s">whitespace.
-      <BR><INPUT TYPE="submit" VALUE="Submit">
-      <BR><TEXTAREA NAME="counties" ROWS=100></TEXTAREA>
-    </FORM>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/cust_main_county.cgi b/htdocs/edit/cust_main_county.cgi
deleted file mode 100755 (executable)
index 904d583..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_main_county.cgi: Edit tax rates (output form)
-#
-# ivan@sisd.com 97-dec-13-16
-#
-# Changes to allow page to work at a relative position in server
-# Changed tax field to accept 6 chars (MO uses 6.1%)
-#      bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-print header("Edit tax rates", menubar(
-  'Main Menu' => '../',
-)),<<END;
-    <FORM ACTION="process/cust_main_county.cgi" METHOD=POST>
-    <TABLE BORDER>
-      <TR>
-        <TH><FONT SIZE=-1>State</FONT></TH>
-        <TH>County</TH>
-        <TH><FONT SIZE=-1>Tax</FONT></TH>
-      </TR>
-END
-
-my($cust_main_county);
-foreach $cust_main_county ( qsearch('cust_main_county',{}) ) {
-  my($hashref)=$cust_main_county->hashref;
-  print <<END;
-      <TR>
-        <TD>$hashref->{state}</TD>
-END
-
-  print "<TD>", $hashref->{county}
-      ? $hashref->{county}
-      : '(ALL)'
-    , "</TD>";
-
-  print qq!<TD><INPUT TYPE="text" NAME="tax!, $hashref->{taxnum},
-        qq!" VALUE="!, $hashref->{tax}, qq!" SIZE=6 MAXLENGTH=6>%</TD></TR>!;
-END
-
-}
-
-print <<END;
-    </TABLE>
-    <INPUT TYPE="submit" VALUE="Apply changes">
-    </FORM>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/cust_pay.cgi b/htdocs/edit/cust_pay.cgi
deleted file mode 100755 (executable)
index a6cb204..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_pay.cgi: Add a payment (output form)
-#
-# Usage: cust_pay.cgi invnum
-#        http://server.name/path/cust_pay.cgi?invnum
-#
-# Note: Should be run setuid as user nobody.
-#
-# some hooks for modifications as well as additions, but needs work.
-#
-# ivan@voicenet.com 96-dec-11
-#
-# rewrite ivan@sisd.com 98-mar-16
-
-use strict;
-use Date::Format;
-use CGI::Base qw(:DEFAULT :CGI);
-use FS::UID qw(cgisuidsetup);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-cgisuidsetup($cgi);
-
-#untaint invnum
-$QUERY_STRING =~ /^(\d+)$/;
-my($invnum)=$1;
-
-SendHeaders(); # one guess.
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Enter payment</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Enter payment</H1>
-    </CENTER>
-    <FORM ACTION="process/cust_pay.cgi" METHOD=POST>
-    <HR><PRE>
-END
-
-#invnum
-print qq!Invoice #<B>$invnum</B><INPUT TYPE="hidden" NAME="invnum" VALUE="$invnum">!;
-
-#date
-my($date)=time;
-print qq!<BR>Date: <B>!, time2str("%D",$date), qq!</B><INPUT TYPE="hidden" NAME="_date" VALUE="$date">!;
-
-#paid
-print qq!<BR>Amount \$<INPUT TYPE="text" NAME="paid" VALUE="" SIZE=8 MAXLENGTH=8>!;
-
-#payby
-my($payby)="BILL";
-print qq!<BR>Payby: <B>$payby</B><INPUT TYPE="hidden" NAME="payby" VALUE="$payby">!;
-
-#payinfo (check # now as payby="BILL" hardcoded.. what to do later?)
-my($payinfo)="";
-print qq!<BR>Check #<INPUT TYPE="text" NAME="payinfo" VALUE="$payinfo">!;
-
-#paybatch
-print qq!<INPUT TYPE="hidden" NAME="paybatch" VALUE="">!;
-
-print <<END;
-</PRE>
-<BR>
-<CENTER><INPUT TYPE="submit" VALUE="Post"></CENTER>
-END
-
-print <<END;
-
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/cust_pkg.cgi b/htdocs/edit/cust_pkg.cgi
deleted file mode 100755 (executable)
index d7f143d..0000000
+++ /dev/null
@@ -1,137 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_pkg.cgi: Add/edit packages (output form)
-#
-# this is for changing packages around, not editing things within the package
-#
-# Usage: cust_pkg.cgi custnum
-#        http://server.name/path/cust_pkg.cgi?custnum
-#
-# Note: Should be run setuid freeside as user nobody
-#
-# started with /sales/add/cust_pkg.cgi, which added packages
-# ivan@voicenet.com 97-jan-5, 97-mar-21
-#
-# Rewrote for new API
-# ivan@voicenet.com 97-jul-7
-#
-# FS::Search is no more, &cgisuidsetup needs $cgi, ivan@sisd.com 98-mar-7 
-#
-# Changes to allow page to work at a relative position in server
-# Changed to display packages 2-wide in a table
-#       bmccane@maxbaud.net     98-apr-3
-#
-# fixed a pretty cool bug from above which caused a visual glitch ivan@sisd.com
-# 98-jun-1
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI); # CGI module
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup getotaker);
-use FS::Record qw(qsearch qsearchs);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-
-my(%pkg,%comment);
-foreach (qsearch('part_pkg', {})) {
-  $pkg{ $_ -> getfield('pkgpart') } = $_->getfield('pkg');
-  $comment{ $_ -> getfield('pkgpart') } = $_->getfield('comment');
-}
-
-#untaint custnum
-$QUERY_STRING =~ /^(\d+)$/;
-my($custnum)=$1;
-
-my($otaker)=&getotaker;
-
-SendHeaders();
-print <<END;
-<HTML>   
-  <HEAD>
-    <TITLE>Add/Edit Packages</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Add/Edit Packages</H1>
-    </CENTER>
-    <FORM ACTION="process/cust_pkg.cgi" METHOD=POST>
-    <HR>
-END
-
-#custnum
-print qq!<INPUT TYPE="hidden" NAME="new_custnum" VALUE="$custnum">!;
-
-#current packages (except cancelled packages)
-my(@cust_pkg) = grep ! $_->getfield('cancel'),
-  qsearch('cust_pkg',{'custnum'=>$custnum});
-
-if (@cust_pkg) {
-  print <<END;
-<CENTER><FONT SIZE="+2">Current packages</FONT></CENTER>
-These are packages the customer currently has.  Select those packages you
-wish to remove (if any).<BR><BR>
-END
-
-  my ($count) = 0 ;
-  print qq!<CENTER><TABLE>! ;
-  foreach (@cust_pkg) {
-    print qq!<TR>! if ($count ==0) ;
-    my($pkgnum,$pkgpart)=( $_->getfield('pkgnum'), $_->getfield('pkgpart') );
-    print qq!<TD><INPUT TYPE="checkbox" NAME="remove_pkg" VALUE="$pkgnum">!,
-          #qq!$pkgnum: $pkg{$pkgpart} - $comment{$pkgpart}</TD>\n!,
-          #now you've got to admit this bug was pretty cool
-          qq!$pkgnum: $pkg{$pkgpart} - $comment{$pkgpart}</TD>\n!;
-    $count ++ ;
-    if ($count == 2)
-    {
-      $count = 0 ;
-      print qq!</TR>\n! ;
-    }
-  }
-  print qq!</TABLE></CENTER>! ;
-
-  print "<HR>";
-}
-
-print <<END;
-<CENTER><FONT SIZE="+2">New packages</FONT></CENTER>
-These are packages the customer can purchase.  Specify the quantity to add
-of each package.<BR><BR>
-END
-
-my($cust_main)=qsearchs('cust_main',{'custnum'=>$custnum});
-my($agent)=qsearchs('agent',{'agentnum'=> $cust_main->agentnum });
-
-my($type_pkgs);
-my ($count) = 0 ;
-print qq!<CENTER><TABLE>! ;
-foreach $type_pkgs ( qsearch('type_pkgs',{'typenum'=> $agent->typenum }) ) {
-  my($pkgpart)=$type_pkgs->pkgpart;
-  print qq!<TR>! if ($count == 0) ;
-  print <<END;
-  <TD>
-  <INPUT TYPE="text" NAME="pkg$pkgpart" VALUE="0" SIZE="2" MAXLENGTH="2">
-  $pkgpart: $pkg{$pkgpart} - $comment{$pkgpart}</TD>\n
-END
-  $count ++ ;
-  if ($count == 2)
-  {
-    print qq!</TR>\n! ;
-    $count = 0 ;
-  }
-}
-print qq!</TABLE></CENTER>! ;
-
-#otaker
-print qq!<INPUT TYPE="hidden" NAME="new_otaker" VALUE="$otaker">\n!;
-
-#submit
-print qq!<P><CENTER><INPUT TYPE="submit" VALUE="Order"></CENTER>\n!;
-
-print <<END;
-    </FORM>
-  </BODY>
-</HTML>
-END
diff --git a/htdocs/edit/part_pkg.cgi b/htdocs/edit/part_pkg.cgi
deleted file mode 100755 (executable)
index 9fe739b..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# part_pkg.cgi: Add/Edit package (output form)
-#
-# ivan@sisd.com 97-dec-10
-#
-# Changes to allow page to work at a relative position in server
-# Changed to display services 2-wide in table
-#       bmccane@maxbaud.net     98-apr-3
-#
-# use FS::CGI, added inline documentation ivan@sisd.com 98-jul-12
-
-use strict;
-use CGI::Base;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::part_pkg;
-use FS::pkg_svc;
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-my($part_pkg,$action);
-if ( $cgi->var('QUERY_STRING') =~ /^(\d+)$/ ) { #editing
-  $part_pkg=qsearchs('part_pkg',{'pkgpart'=>$1});
-  $action='Edit';
-} else { #adding
-  $part_pkg=create FS::part_pkg {};
-  $action='Add';
-}
-my($hashref)=$part_pkg->hashref;
-
-print header("$action Package Definition", menubar(
-  'Main Menu' => '../',
-  'View all packages' => '../browse/part_pkg.cgi',
-)), '<FORM ACTION="process/part_pkg.cgi" METHOD=POST>';
-
-print qq!<INPUT TYPE="hidden" NAME="pkgpart" VALUE="$hashref->{pkgpart}">!,
-      "Package Part #", $hashref->{pkgpart} ? $hashref->{pkgpart} : "(NEW)";
-
-print <<END;
-<PRE>
-Package (customer-visable)          <INPUT TYPE="text" NAME="pkg" SIZE=32 VALUE="$hashref->{pkg}">
-Comment (customer-hidden)           <INPUT TYPE="text" NAME="comment" SIZE=32 VALUE="$hashref->{comment}">
-Setup fee for this package          <INPUT TYPE="text" NAME="setup" VALUE="$hashref->{setup}">
-Recurring fee for this package      <INPUT TYPE="text" NAME="recur" VALUE="$hashref->{recur}">
-Frequency (months) of recurring fee <INPUT TYPE="text" NAME="freq" VALUE="$hashref->{freq}">
-
-</PRE>
-
-Enter the quantity of each service this package includes.<BR><BR>
-<TABLE BORDER><TR><TH><FONT SIZE=-1>Quan.</FONT></TH><TH>Service</TH>
-                 <TH><FONT SIZE=-1>Quan.</FONT></TH><TH>Service</TH></TR>
-END
-
-my($part_svc);
-my($count) = 0 ;
-foreach $part_svc ( qsearch('part_svc',{}) ) {
-
-  my($svcpart)=$part_svc->getfield('svcpart');
-  my($pkg_svc)=qsearchs('pkg_svc',{
-    'pkgpart'  => $part_pkg->getfield('pkgpart'),
-    'svcpart'  => $svcpart,
-  })  || create FS::pkg_svc({
-    'pkgpart'  => $part_pkg->getfield('pkgpart'),
-    'svcpart'  => $svcpart,
-    'quantity' => 0,
-  });
-  next unless $pkg_svc;
-
-  print qq!<TR>! if $count == 0 ;
-  print qq!<TD><INPUT TYPE="text" NAME="pkg_svc$svcpart" SIZE=3 VALUE="!,
-        $pkg_svc->getfield('quantity') || 0,qq!"></TD>!,
-        qq!<TD><A HREF="part_svc.cgi?!,$part_svc->getfield('svcpart'),
-        qq!">!, $part_svc->getfield('svc'), "</A></TD>";
-  $count ++ ;
-  if ($count == 2)
-  {
-    print qq!</TR>! ;
-    $count = 0 ;
-  }
-}
-print qq!</TR>! if ($count != 0) ;
-
-print "</TABLE>";
-
-print qq!<BR><INPUT TYPE="submit" VALUE="!,
-      $hashref->{pkgpart} ? "Apply changes" : "Add package",
-      qq!">!;
-
-print <<END;
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/part_referral.cgi b/htdocs/edit/part_referral.cgi
deleted file mode 100755 (executable)
index f298022..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# agent.cgi: Add/Edit referral (output form)
-#
-# ivan@sisd.com 98-feb-23
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# confisuing typo on submit button ivan@sisd.com 98-jun-14
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::part_referral;
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-my($part_referral,$action);
-if ( $cgi->var('QUERY_STRING') =~ /^(\d+)$/ ) { #editing
-  $part_referral=qsearchs('part_referral',{'refnum'=>$1});
-  $action='Edit';
-} else { #adding
-  $part_referral=create FS::part_referral {};
-  $action='Add';
-}
-my($hashref)=$part_referral->hashref;
-
-print header("$action Referral", menubar(
-  'Main Menu' => '../',
-  'View all referrals' => "../browse/part_referral.cgi",
-)), <<END;
-    <FORM ACTION="process/part_referral.cgi" METHOD=POST>
-END
-
-#display
-
-print qq!<INPUT TYPE="hidden" NAME="refnum" VALUE="$hashref->{refnum}">!,
-      "Referral #", $hashref->{refnum} ? $hashref->{refnum} : "(NEW)";
-
-print <<END;
-<PRE>
-Referral   <INPUT TYPE="text" NAME="referral" SIZE=32 VALUE="$hashref->{referral}">
-</PRE>
-END
-
-print qq!<BR><INPUT TYPE="submit" VALUE="!,
-      $hashref->{refnum} ? "Apply changes" : "Add referral",
-      qq!">!;
-
-print <<END;
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/part_svc.cgi b/htdocs/edit/part_svc.cgi
deleted file mode 100755 (executable)
index 491c013..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# part_svc.cgi: Add/Edit service (output form)
-#
-# ivan@sisd.com 97-nov-14
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# use FS::CGI, added inline documentation ivan@sisd.com 98-jul-12
-
-use strict;
-use CGI::Base;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::part_svc qw(fields);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-my($part_svc,$action);
-if ( $cgi->var('QUERY_STRING') =~ /^(\d+)$/ ) { #editing
-  $part_svc=qsearchs('part_svc',{'svcpart'=>$1});
-  $action='Edit';
-} else { #adding
-  $part_svc=create FS::part_svc {};
-  $action='Add';
-}
-my($hashref)=$part_svc->hashref;
-
-print header("$action Service Definition", menubar(
-  'Main Menu' => '../',
-  'View all services' => '../browse/part_svc.cgi',
-)), '<FORM ACTION="process/part_svc.cgi" METHOD=POST>';
-
-
-
-print qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$hashref->{svcpart}">!,
-      "Service Part #", $hashref->{svcpart} ? $hashref->{svcpart} : "(NEW)";
-
-print <<END;
-<PRE>
-Service  <INPUT TYPE="text" NAME="svc" VALUE="$hashref->{svc}">
-Table    <SELECT NAME="svcdb" SIZE=1>
-END
-
-print map '<OPTION'. ' SELECTED'x($_ eq $hashref->{svcdb}). ">$_\n", qw(
-  svc_acct svc_domain svc_acct_sm svc_charge svc_wo
-);
-
-print <<END;
-</SELECT></PRE>
-Services are items you offer to your customers.
-<UL><LI>svc_acct - Shell accounts, POP mailboxes, SLIP/PPP and ISDN accounts
-    <LI>svc_domain - Virtual domains
-    <LI>svc_acct_sm - Virtual domain mail aliasing
-    <LI>svc_charge - One-time charges (Partially unimplemented)
-    <LI>svc_wo - Work orders (Partially unimplemented)
-</UL>
-For the columns in the table selected above, you can set default or fixed 
-values.  For example, a SLIP/PPP account may have a default (or perhaps fixed)
-<B>slipip</B> of <B>0.0.0.0</B>, while a POP mailbox will probably have a fixed
-blank <B>slipip</B> as well as a fixed shell something like <B>/bin/true</B> or
-<B>/usr/bin/passwd</B>.
-<BR><BR>
-<TABLE BORDER CELLPADDING=4><TR><TH>Table</TH><TH>Field</TH>
-<TH COLSPAN=2>Modifier</TH></TR>
-END
-
-#these might belong somewhere else for other user interfaces 
-#pry need to eventually create stuff that's shared amount UIs
-my(%defs)=(
-  'svc_acct' => {
-    'dir'       => 'Home directory',
-    'uid'       => 'UID (set to fixed and blank for dial-only)',
-    'slipip'    => 'IP address',
-    'popnum'    => '<A HREF="../browse/svc_acct_pop.cgi/">POP number</A>',
-    'username'  => 'Username',
-    'quota'     => '(unimplemented)',
-    '_password' => 'Password',
-    'gid'       => 'GID (when blank, defaults to UID)',
-    'shell'     => 'Shell',
-    'finger'    => 'GECOS',
-  },
-  'svc_domain' => {
-    'domain'    => 'Domain',
-  },
-  'svc_acct_sm' => {
-    'domuser'   => 'domuser@virtualdomain.com',
-    'domuid'    => 'UID where domuser@virtualdomain.com mail is forwarded',
-    'domsvc'    => 'svcnum from svc_domain for virtualdomain.com',
-  },
-  'svc_charge' => {
-    'amount'    => 'amount',
-  },
-  'svc_wo' => {
-    'worker'    => 'Worker',
-    '_date'      => 'Date',
-  },
-);
-
-my($svcdb);
-foreach $svcdb ( qw(
-  svc_acct svc_domain svc_acct_sm svc_charge svc_wo
-) ) {
-
-  my(@rows)=map { /^${svcdb}__(.*)$/; $1 }
-    grep ! /_flag$/,
-      grep /^${svcdb}__/,
-        fields('part_svc');
-  my($rowspan)=scalar(@rows);
-
-  my($ptmp)="<TD ROWSPAN=$rowspan>$svcdb</TD>";
-  my($row);
-  foreach $row (@rows) {
-    my($value)=$part_svc->getfield($svcdb.'__'.$row);
-    my($flag)=$part_svc->getfield($svcdb.'__'.$row.'_flag');
-    print "<TR>$ptmp<TD>$row - <FONT SIZE=-1>$defs{$svcdb}{$row}</FONT></TD>";
-    print qq!<TD><INPUT TYPE="radio" NAME="${svcdb}__${row}_flag" VALUE=""!.
-      ' CHECKED'x($flag eq ''). "><BR>Off</TD>";
-    print qq!<TD><INPUT TYPE="radio" NAME="${svcdb}__${row}_flag" VALUE="D"!.
-      ' CHECKED'x($flag eq 'D'). ">Default ";
-    print qq!<INPUT TYPE="radio" NAME="${svcdb}__${row}_flag" VALUE="F"!.
-      ' CHECKED'x($flag eq 'F'). ">Fixed ";
-    print qq!<BR><INPUT TYPE="text" NAME="${svcdb}__${row}" VALUE="$value">!,
-      "</TD></TR>";
-    $ptmp='';
-  }
-}
-print "</TABLE>";
-
-print qq!\n<CENTER><BR><INPUT TYPE="submit" VALUE="!,
-      $hashref->{svcpart} ? "Apply changes" : "Add service",
-      qq!"></CENTER>!;
-
-print <<END;
-
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/process/agent.cgi b/htdocs/edit/process/agent.cgi
deleted file mode 100755 (executable)
index 5d1ce32..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/agent.cgi: Edit agent (process form)
-#
-# ivan@sisd.com 97-dec-12
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::agent qw(fields);
-use FS::CGI qw(idiot);
-
-my($req)=new CGI::Request; # create form object
-
-&cgisuidsetup($req->cgi);
-
-my($agentnum)=$req->param('agentnum');
-
-my($old)=qsearchs('agent',{'agentnum'=>$agentnum}) if $agentnum;
-
-#unmunge typenum
-$req->param('typenum') =~ /^(\d+)(:.*)?$/;
-$req->param('typenum',$1);
-
-my($new)=create FS::agent ( {
-  map {
-    $_, $req->param($_);
-  } fields('agent')
-} );
-
-my($error);
-if ( $agentnum ) {
-  $error=$new->replace($old);
-} else {
-  $error=$new->insert;
-  $agentnum=$new->getfield('agentnum');
-}
-
-if ( $error ) {
-  &idiot($error);
-} else { 
-  #$req->cgi->redirect("../../view/agent.cgi?$agentnum");
-  #$req->cgi->redirect("../../edit/agent.cgi?$agentnum");
-  $req->cgi->redirect("../../browse/agent.cgi");
-}
-
diff --git a/htdocs/edit/process/agent_type.cgi b/htdocs/edit/process/agent_type.cgi
deleted file mode 100755 (executable)
index 43f129f..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/agent_type.cgi: Edit agent type (process form)
-#
-# ivan@sisd.com 97-dec-11
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::agent_type qw(fields);
-use FS::type_pkgs;
-use FS::CGI qw(idiot);
-
-my($req)=new CGI::Request;
-&cgisuidsetup($req->cgi);
-
-my($typenum)=$req->param('typenum');
-my($old)=qsearchs('agent_type',{'typenum'=>$typenum}) if $typenum;
-
-my($new)=create FS::agent_type ( {
-  map {
-    $_, $req->param($_);
-  } fields('agent_type')
-} );
-
-my($error);
-if ( $typenum ) {
-  $error=$new->replace($old);
-} else {
-  $error=$new->insert;
-  $typenum=$new->getfield('typenum');
-}
-
-if ( $error ) {
-  idiot($error);
-  exit;
-}
-
-my($part_pkg);
-foreach $part_pkg (qsearch('part_pkg',{})) {
-  my($pkgpart)=$part_pkg->getfield('pkgpart');
-
-  my($type_pkgs)=qsearchs('type_pkgs',{
-      'typenum' => $typenum,
-      'pkgpart' => $pkgpart,
-  });
-  if ( $type_pkgs && ! $req->param("pkgpart$pkgpart") ) {
-    my($d_type_pkgs)=$type_pkgs; #need to save $type_pkgs for below.
-    $error=$d_type_pkgs->del; #FS::Record not FS::type_pkgs,
-                                  #so ->del not ->delete.  hmm.  hmm.
-    if ( $error ) {
-      idiot($error);
-      exit;
-    }
-
-  } elsif ( $req->param("pkgpart$pkgpart")
-            && ! $type_pkgs
-  ) {
-    #ok to clobber it now (but bad form nonetheless?)
-    $type_pkgs=create FS::type_pkgs ({
-      'typenum' => $typenum,
-      'pkgpart' => $pkgpart,
-    });
-    $error= $type_pkgs->insert;
-    if ( $error ) {
-      idiot($error);
-      exit;
-    }
-  }
-
-}
-
-#$req->cgi->redirect("../../view/agent_type.cgi?$typenum");
-#$req->cgi->redirect("../../edit/agent_type.cgi?$typenum");
-$req->cgi->redirect("../../browse/agent_type.cgi");
-
diff --git a/htdocs/edit/process/cust_credit.cgi b/htdocs/edit/process/cust_credit.cgi
deleted file mode 100755 (executable)
index e660b4c..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/cust_credit.cgi: Add a credit (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/cust_credit.cgi
-#
-# Note: Should be run setuid root as user nobody.
-#
-# ivan@voicenet.com 96-dec-05 -> 96-dec-08
-#
-# post a refund if $new_paybatch
-# ivan@voicenet.com 96-dec-08
-#
-# refunds are no longer applied against a specific payment (paybatch)
-# paybatch field removed
-# ivan@voicenet.com 97-apr-22
-#
-# rewrite ivan@sisd.com 98-mar-16
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Request;
-use FS::UID qw(cgisuidsetup getotaker);
-use FS::cust_credit;
-
-my($req)=new CGI::Request; # create form object
-cgisuidsetup($req->cgi);
-
-$req->param('custnum') =~ /^(\d*)$/ or die "Illegal custnum!";
-my($custnum)=$1;
-
-$req->param('otaker',getotaker);
-
-my($new) = create FS::cust_credit ( {
-  map {
-    $_, $req->param($_);
-  } qw(custnum _date amount otaker reason)
-} );
-
-my($error);
-$error=$new->insert;
-&idiot($error) if $error;
-
-#no errors, no refund, so view our credit.
-$req->cgi->redirect("../../view/cust_main.cgi?$custnum#history");
-
-sub idiot {
-  my($error)=@_;
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error posting credit/refund</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error posting credit/refund</H4>
-    </CENTER>
-    Your update did not occur because of the following error:
-    <P><B>$error</B>
-    <P>Hit the <I>Back</I> button in your web browser, correct this mistake, and press the <I>Post</I> button again.
-  </BODY>
-</HTML>
-END
-
-}
-
diff --git a/htdocs/edit/process/cust_main.cgi b/htdocs/edit/process/cust_main.cgi
deleted file mode 100755 (executable)
index 7664dfc..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/cust_main.cgi: Edit a customer (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/cust_main.cgi
-#
-# Note: Should be run setuid root as user nobody.
-#
-# ivan@voicenet.com 96-dec-04
-#
-# added referral check
-# ivan@voicenet.com 97-jun-4
-#
-# rewrote for new API
-# ivan@voicenet.com 97-jul-28
-#
-# same as above (again) and clean up some stuff ivan@sisd.com 98-feb-23
-#
-# Changes to allow page to work at a relative position in server
-# Changed 'day' to 'daytime' because Pg6.3 reserves the day word
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::cust_main;
-
-my($req)=new CGI::Request; # create form object
-
-&cgisuidsetup($req->cgi);
-
-#create new record object
-
-#unmunge agentnum
-$req->param('agentnum', 
-  (split(/:/, ($req->param('agentnum'))[0] ))[0]
-);
-
-#unmunge tax
-$req->param('tax','') unless defined($req->param('tax'));
-
-#unmunge refnum
-$req->param('refnum',
-  (split(/:/, ($req->param('refnum'))[0] ))[0]
-);
-
-#unmunge state/county
-$req->param('state') =~ /^(\w+)( \((\w+)\))?$/;
-$req->param('state', $1);
-$req->param('county', $3 || '');
-
-my($new) = create FS::cust_main ( {
-  map {
-    $_, $req->param("$_") || ''
-  } qw(custnum agentnum last first ss company address1 address2 city county
-       state zip country daytime night fax payby payinfo paydate payname tax
-       otaker refnum)
-} );
-
-if ( $new->custnum eq '' ) {
-
-  my($error)=$new->insert;
-  &idiot($error) if $error;
-
-} else { #create old record object
-
-  my($old) = qsearchs( 'cust_main', { 'custnum', $new->custnum } ); 
-  &idiot("Old record not found!") unless $old;
-  my($error)=$new->replace($old);
-  &idiot($error) if $error;
-
-}
-
-my($custnum)=$new->custnum;
-$req->cgi->redirect("../../view/cust_main.cgi?$custnum#cust_main");
-
-sub idiot {
-  my($error)=@_;
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error updating customer information</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error updating customer information</H4>
-    </CENTER>
-    Your update did not occur because of the following error:
-    <P><B>$error</B>
-    <P>Hit the <I>Back</I> button in your web browser, correct this mistake, and submit the form again.
-  </BODY>
-</HTML>
-END
-
-  exit;
-
-}
-
diff --git a/htdocs/edit/process/cust_main_county-expand.cgi b/htdocs/edit/process/cust_main_county-expand.cgi
deleted file mode 100755 (executable)
index a821560..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/cust_main_county-expand.cgi: Expand counties (process form)
-#
-# ivan@sisd.com 97-dec-16
-#
-# Changes to allow page to work at a relative position in server
-# Added import of datasrc from UID.pm for Pg6.3
-# Default tax to 0.0 if using Pg6.3
-#       bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI
-# undo default tax to 0.0 if using Pg6.3: comes from pre-expanded record
-# for that state
-#ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup datasrc);
-use FS::Record qw(qsearch qsearchs);
-use FS::cust_main_county;
-use FS::CGI qw(eidiot);
-
-my($req)=new CGI::Request; # create form object
-
-&cgisuidsetup($req->cgi);
-
-$req->param('taxnum') =~ /^(\d+)$/ or die "Illegal taxnum!";
-my($taxnum)=$1;
-my($cust_main_county)=qsearchs('cust_main_county',{'taxnum'=>$taxnum})
-  or die ("Unknown taxnum!");
-
-my(@counties);
-if ( $req->param('delim') eq 'n' ) {
-  @counties=split(/\n/,$req->param('counties'));
-} elsif ( $req->param('delim') eq 's' ) {
-  @counties=split(/\s+/,$req->param('counties'));
-} else {
-  die "Illegal delim!";
-}
-
-@counties=map {
-  /^\s*([\w\- ]+)\s*$/ or eidiot("Illegal county");
-  $1;
-} @counties;
-
-my($county);
-foreach ( @counties) {
-  my(%hash)=$cust_main_county->hash;
-  my($new)=create FS::cust_main_county \%hash;
-  $new->setfield('taxnum','');
-  $new->setfield('county',$_);
-  #if (datasrc =~ m/Pg/)
-  #{
-  #    $new->setfield('tax',0.0);
-  #}
-  my($error)=$new->insert;
-  die $error if $error;
-}
-
-unless ( qsearch('cust_main',{
-  'state'  => $cust_main_county->getfield('state'),
-  'county' => $cust_main_county->getfield('county'),
-} ) ) {
-  my($error)=($cust_main_county->delete);
-  die $error if $error;
-}
-
-$req->cgi->redirect("../../edit/cust_main_county.cgi");
-
diff --git a/htdocs/edit/process/cust_main_county.cgi b/htdocs/edit/process/cust_main_county.cgi
deleted file mode 100755 (executable)
index 58eaa63..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/agent.cgi: Edit cust_main_county (process form)
-#
-# ivan@sisd.com 97-dec-16
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::cust_main_county;
-use FS::CGI qw(eidiot);
-
-my($req)=new CGI::Request; # create form object
-
-&cgisuidsetup($req->cgi);
-
-foreach ( $req->params ) {
-  /^tax(\d+)$/ or die "Illegal form $_!";
-  my($taxnum)=$1;
-  my($old)=qsearchs('cust_main_county',{'taxnum'=>$taxnum})
-    or die "Couldn't find taxnum $taxnum!";
-  next unless $old->getfield('tax') ne $req->param("tax$taxnum");
-  my(%hash)=$old->hash;
-  $hash{tax}=$req->param("tax$taxnum");
-  my($new)=create FS::cust_main_county \%hash;
-  my($error)=$new->replace($old);
-  eidiot($error) if $error;
-}
-
-$req->cgi->redirect("../../browse/cust_main_county.cgi");
-
diff --git a/htdocs/edit/process/cust_pay.cgi b/htdocs/edit/process/cust_pay.cgi
deleted file mode 100755 (executable)
index 9ec9753..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/cust_pay.cgi: Add a payment (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/cust_pay.cgi
-#
-# Note: Should be run setuid root as user nobody.
-#
-# ivan@voicenet.com 96-dec-11
-#
-# rewrite ivan@sisd.com 98-mar-16
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Request;
-use FS::UID qw(cgisuidsetup);
-use FS::cust_pay qw(fields);
-
-my($req)=new CGI::Request;
-&cgisuidsetup($req->cgi);
-
-$req->param('invnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
-my($invnum)=$1;
-
-my($new) = create FS::cust_pay ( {
-  map {
-    $_, $req->param($_);
-  } qw(invnum paid _date payby payinfo paybatch)
-} );
-
-my($error);
-$error=$new->insert;
-
-if ($error) { #error!
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error posting payment</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error posting payment</H4>
-    </CENTER>
-    Your update did not occur because of the following error:
-    <P><B>$error</B>
-    <P>Hit the <I>Back</I> button in your web browser, correct this mistake, and press the <I>Post</I> button again.
-  </BODY>
-</HTML>
-END
-} else { #no errors!
-  $req->cgi->redirect("../../view/cust_bill.cgi?$invnum");
-}
-
diff --git a/htdocs/edit/process/cust_pkg.cgi b/htdocs/edit/process/cust_pkg.cgi
deleted file mode 100755 (executable)
index 6f5bc87..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/cust_pkg.cgi: Add/edit packages (process form)
-#
-# this is for changing packages around, not for editing things within the
-# package
-#
-# Usage: post form to:
-#        http://server.name/path/cust_pkg.cgi
-#
-# Note: Should be run setuid root as user nobody.
-#
-# ivan@voicenet.com 97-mar-21 - 97-mar-24
-#
-# rewrote for new API
-# ivan@voicenet.com 97-jul-7 - 15
-#
-# &cgisuidsetup($cgi) ivan@sisd.com 98-mar-7
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::cust_pkg;
-
-my($req)=new CGI::Request; # create form object
-
-&cgisuidsetup($req->cgi);
-
-#untaint custnum
-$req->param('new_custnum') =~ /^(\d+)$/;
-my($custnum)=$1;
-
-my(@remove_pkgnums) = map {
-  /^(\d+)$/ or die "Illegal remove_pkg value!";
-  $1;
-} $req->param('remove_pkg');
-
-my(@pkgparts);
-my($pkgpart);
-foreach $pkgpart ( map /^pkg(\d+)$/ ? $1 : (), $req->params ) {
-  my($num_pkgs)=$req->param("pkg$pkgpart");
-  while ( $num_pkgs-- ) {
-    push @pkgparts,$pkgpart;
-  }
-}
-
-my($error) = FS::cust_pkg::order($custnum,\@pkgparts,\@remove_pkgnums);
-
-if ($error) {
-  CGI::Base::SendHeaders();
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error updating packages</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error updating packages</H4>
-    </CENTER>
-    Your update did not occur because of the following error:
-    <P><B>$error</B>
-    <P>Hit the <I>Back</I> button in your web browser, correct this mistake, and submit the form again.
-  </BODY>
-</HTML>
-END
-} else {
-  $req->cgi->redirect("../../view/cust_main.cgi?$custnum#cust_pkg");
-}
-
diff --git a/htdocs/edit/process/part_pkg.cgi b/htdocs/edit/process/part_pkg.cgi
deleted file mode 100755 (executable)
index 7d78781..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/part_pkg.cgi: Edit package definitions (process form)
-#
-# ivan@sisd.com 97-dec-10
-#
-# don't update non-changing records in part_svc (causing harmless but annoying
-# "Records identical" errors). ivan@sisd.com 98-feb-19
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# Added `|| 0 ' when getting quantity off web page ivan@sisd.com 98-jun-4
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::part_pkg qw(fields);
-use FS::pkg_svc;
-use FS::CGI qw(eidiot);
-
-my($req)=new CGI::Request; # create form object
-
-&cgisuidsetup($req->cgi);
-
-my($pkgpart)=$req->param('pkgpart');
-
-my($old)=qsearchs('part_pkg',{'pkgpart'=>$pkgpart}) if $pkgpart;
-
-my($new)=create FS::part_pkg ( {
-  map {
-    $_, $req->param($_);
-  } fields('part_pkg')
-} );
-
-if ( $pkgpart ) {
-  my($error)=$new->replace($old);
-  eidiot($error) if $error;
-} else {
-  my($error)=$new->insert;
-  eidiot($error) if $error;
-  $pkgpart=$new->getfield('pkgpart');
-}
-
-my($part_svc);
-foreach $part_svc (qsearch('part_svc',{})) {
-# don't update non-changing records in part_svc (causing harmless but annoying
-# "Records identical" errors). ivan@sisd.com 98-jan-19
-  #my($quantity)=$req->param('pkg_svc'. $part_svc->getfield('svcpart')),
-  my($quantity)=$req->param('pkg_svc'. $part_svc->svcpart) || 0,
-  my($old_pkg_svc)=qsearchs('pkg_svc',{
-    'pkgpart'  => $pkgpart,
-    'svcpart'  => $part_svc->getfield('svcpart'),
-  });
-  my($old_quantity)=$old_pkg_svc ? $old_pkg_svc->quantity : 0;
-  next unless $old_quantity != $quantity; #!here
-  my($new_pkg_svc)=create FS::pkg_svc({
-    'pkgpart'  => $pkgpart,
-    'svcpart'  => $part_svc->getfield('svcpart'),
-    #'quantity' => $req->param('pkg_svc'. $part_svc->getfield('svcpart')),
-    'quantity' => $quantity, 
-  });
-  if ($old_pkg_svc) {
-    my($error)=$new_pkg_svc->replace($old_pkg_svc);
-    eidiot($error) if $error;
-  } else {
-    my($error)=$new_pkg_svc->insert;
-    eidiot($error) if $error;
-  }
-}
-
-#$req->cgi->redirect("../../view/part_pkg.cgi?$pkgpart");
-#$req->cgi->redirect("../../edit/part_pkg.cgi?$pkgpart");
-$req->cgi->redirect("../../browse/part_pkg.cgi");
-
diff --git a/htdocs/edit/process/part_referral.cgi b/htdocs/edit/process/part_referral.cgi
deleted file mode 100755 (executable)
index 08a4c01..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/part_referral.cgi: Edit referrals (process form)
-#
-# ivan@sisd.com 98-feb-23
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::part_referral qw(fields);
-use FS::CGI qw(eidiot);
-use FS::CGI qw(eidiot);
-
-my($req)=new CGI::Request; # create form object
-
-&cgisuidsetup($req->cgi);
-
-my($refnum)=$req->param('refnum');
-
-my($new)=create FS::part_referral ( {
-  map {
-    $_, $req->param($_);
-  } fields('part_referral')
-} );
-
-if ( $refnum ) {
-  my($old)=qsearchs('part_referral',{'refnum'=>$refnum});
-  eidiot("(Old) Record not found!") unless $old;
-  my($error)=$new->replace($old);
-  eidiot($error) if $error;
-} else {
-  my($error)=$new->insert;
-  eidiot($error) if $error;
-}
-
-$refnum=$new->getfield('refnum');
-$req->cgi->redirect("../../browse/part_referral.cgi");
-
diff --git a/htdocs/edit/process/part_svc.cgi b/htdocs/edit/process/part_svc.cgi
deleted file mode 100755 (executable)
index 0f0fbc6..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/part_svc.cgi: Edit service definitions (process form)
-#
-# ivan@sisd.com 97-nov-14
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::part_svc qw(fields);
-use FS::CGI qw(eidiot);
-
-my($req)=new CGI::Request; # create form object
-
-&cgisuidsetup($req->cgi);
-
-my($svcpart)=$req->param('svcpart');
-
-my($old)=qsearchs('part_svc',{'svcpart'=>$svcpart}) if $svcpart;
-
-my($new)=create FS::part_svc ( {
-  map {
-    $_, $req->param($_);
-#  } qw(svcpart svc svcdb)
-  } fields('part_svc')
-} );
-
-if ( $svcpart ) {
-  my($error)=$new->replace($old);
-  eidiot($error) if $error;
-} else {
-  my($error)=$new->insert;
-  eidiot($error) if $error;
-  $svcpart=$new->getfield('svcpart');
-}
-
-#$req->cgi->redirect("../../view/part_svc.cgi?$svcpart");
-#$req->cgi->redirect("../../edit/part_svc.cgi?$svcpart");
-$req->cgi->redirect("../../browse/part_svc.cgi");
-
diff --git a/htdocs/edit/process/svc_acct.cgi b/htdocs/edit/process/svc_acct.cgi
deleted file mode 100755 (executable)
index 8d77ba7..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/svc_acct.cgi: Add/edit a customer (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/svc_acct.cgi
-#
-# Note: Should br run setuid root as user nobody.
-#
-# ivan@voicenet.com 96-dec-18
-#
-# Changed /u to /u2
-# ivan@voicenet.com 97-may-6
-#
-# rewrote for new API
-# ivan@voicenet.com 97-jul-17 - 21
-#
-# no FS::Search, FS::svc_acct creates FS::cust_svc record, used for adding
-# and editing ivan@sisd.com 98-mar-8
-#
-# Changes to allow page to work at a relative position in server
-# Changed 'password' to '_password' because Pg6.3 reserves the password word
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::svc_acct;
-
-my($req) = new CGI::Request; # create form object
-&cgisuidsetup($req->cgi);
-
-$req->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
-my($svcnum)=$1;
-
-my($old)=qsearchs('svc_acct',{'svcnum'=>$svcnum}) if $svcnum;
-
-#unmunge popnum
-$req->param('popnum', (split(/:/, $req->param('popnum') ))[0] );
-
-#unmunge passwd
-if ( $req->param('_password') eq '*HIDDEN*' ) {
-  $req->param('_password',$old->getfield('_password'));
-}
-
-my($new) = create FS::svc_acct ( {
-  map {
-    $_, $req->param($_);
-  } qw(svcnum pkgnum svcpart username _password popnum uid gid finger dir
-    shell quota slipip)
-} );
-
-if ( $svcnum ) {
-  my($error) = $new->replace($old);
-  &idiot($error) if $error;
-} else {
-  my($error) = $new->insert;
-  &idiot($error) if $error;
-  $svcnum = $new->getfield('svcnum');
-}
-
-#no errors, view account
-$req->cgi->redirect("../../view/svc_acct.cgi?" . $svcnum );
-
-sub idiot {
-  my($error)=@_;
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error adding/updating account</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error adding/updating account</H4>
-    </CENTER>
-    Your update did not occur because of the following error:
-    <P><B>$error</B>
-    <P>Hit the <I>Back</I> button in your web browser, correct this mistake, and submit the form again.
-  </BODY>
-</HTML>
-END
-  exit;
-}
-
diff --git a/htdocs/edit/process/svc_acct_pop.cgi b/htdocs/edit/process/svc_acct_pop.cgi
deleted file mode 100755 (executable)
index 18d7940..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/svc_acct_pop.cgi: Edit POP (process form)
-#
-# ivan@sisd.com 98-mar-8
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::svc_acct_pop qw(fields);
-use FS::CGI qw(eidiot);
-
-my($req)=new CGI::Request; # create form object
-
-&cgisuidsetup($req->cgi);
-
-my($popnum)=$req->param('popnum');
-
-my($old)=qsearchs('svc_acct_pop',{'popnum'=>$popnum}) if $popnum;
-
-my($new)=create FS::svc_acct_pop ( {
-  map {
-    $_, $req->param($_);
-  } fields('svc_acct_pop')
-} );
-
-if ( $popnum ) {
-  my($error)=$new->replace($old);
-  eidiot($error) if $error;
-} else {
-  my($error)=$new->insert;
-  eidiot($error) if $error;
-  $popnum=$new->getfield('popnum');
-}
-$req->cgi->redirect("../../browse/svc_acct_pop.cgi");
-
diff --git a/htdocs/edit/process/svc_acct_sm.cgi b/htdocs/edit/process/svc_acct_sm.cgi
deleted file mode 100755 (executable)
index 9ad546b..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/svc_acct_sm.cgi: Add/edit a mail alias (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/svc_acct_sm.cgi
-#
-# Note: Should br run setuid root as user nobody.
-#
-# lots of crufty stuff from svc_acct still in here, and modifications are (unelegantly) disabled.
-#
-# ivan@voicenet.com 97-jan-6
-#
-# enabled modifications
-# 
-# ivan@voicenet.com 97-may-7
-#
-# fixed removal of cust_svc record on modifications!
-# ivan@voicenet.com 97-jun-5
-#
-# rewrite ivan@sisd.com 98-mar-15
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::svc_acct_sm;
-
-my($req)=new CGI::Request; # create form object
-cgisuidsetup($req->cgi);
-
-$req->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
-my($svcnum)=$1;
-
-my($old)=qsearchs('svc_acct_sm',{'svcnum'=>$svcnum}) if $svcnum;
-
-#unmunge domsvc and domuid
-$req->param('domsvc',(split(/:/, $req->param('domsvc') ))[0] );
-$req->param('domuid',(split(/:/, $req->param('domuid') ))[0] );
-
-my($new) = create FS::svc_acct_sm ( {
-  map {
-    ($_, scalar($req->param($_)));
-  } qw(svcnum pkgnum svcpart domuser domuid domsvc)
-} );
-
-my($error);
-if ( $svcnum ) {
-  $error = $new->replace($old);
-} else {
-  $error = $new->insert;
-  $svcnum = $new->getfield('svcnum');
-} 
-
-unless ($error) {
-  $req->cgi->redirect("../../view/svc_acct_sm.cgi?$svcnum");
-} else {
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error adding/editing mail alias</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error adding/editing mail alias</H4>
-    </CENTER>
-    Your update did not occur because of the following error:
-    <P><B>$error</B>
-    <P>Hit the <I>Back</I> button in your web browser, correct this mistake, and submit the form again.
-  </BODY>
-</HTML>
-END
-
-}
-
diff --git a/htdocs/edit/process/svc_domain.cgi b/htdocs/edit/process/svc_domain.cgi
deleted file mode 100755 (executable)
index 0782772..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/svc_domain.cgi: Add a domain (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/svc_domain.cgi
-#
-# Note: Should br run setuid root as user nobody.
-#
-# lots of yucky stuff in this one... bleachlkjhui!
-#
-# ivan@voicenet.com 97-jan-6
-#
-# kludged for new domain template 3.5
-# ivan@voicenet.com 97-jul-24
-#
-# moved internic bits to svc_domain.pm ivan@sisd.com 98-mar-14
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::svc_domain;
-
-#remove this to actually test the domains!
-$FS::svc_domain::whois_hack = 1;
-
-my($req) = new CGI::Request;
-&cgisuidsetup($req->cgi);
-
-$req->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
-my($svcnum)=$1;
-
-my($new) = create FS::svc_domain ( {
-  map {
-    $_, $req->param($_);
-  } qw(svcnum pkgnum svcpart domain action purpose)
-} );
-
-my($error);
-if ($req->param('legal') ne "Yes") {
-  $error = "Customer did not agree to be bound by NSI's ".
-    qq!<A HREF="http://rs.internic.net/help/agreement.txt">!.
-    "Domain Name Resgistration Agreement</A>";
-} elsif ($req->param('svcnum')) {
-  $error="Can't modify a domain!";
-} else {
-  $error=$new->insert;
-  $svcnum=$new->svcnum;
-}
-
-unless ($error) {
-  $req->cgi->redirect("../../view/svc_domain.cgi?$svcnum");
-} else {
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error adding domain</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error adding domain</H4>
-    </CENTER>
-    Your update did not occur because of the following error:
-    <P><B>$error</B>
-    <P>Hit the <I>Back</I> button in your web browser, correct this mistake, and submit the form again.
-  </BODY>
-</HTML>
-END
-
-}
-
-
diff --git a/htdocs/edit/svc_acct.cgi b/htdocs/edit/svc_acct.cgi
deleted file mode 100755 (executable)
index 61d0fdc..0000000
+++ /dev/null
@@ -1,191 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# svc_acct.cgi: Add/edit account (output form)
-#
-# Usage: svc_acct.cgi {svcnum} | pkgnum{pkgnum}-svcpart{svcpart}
-#        http://server.name/path/svc_acct.cgi? {svcnum} | pkgnum{pkgnum}-svcpart{svcpart}
-#
-# Note: Should be run setuid freeside as user nobody
-#
-# ivan@voicenet.com 96-dec-18
-#
-# rewrite ivan@sisd.com 98-mar-8
-#
-# Changes to allow page to work at a relative position in server
-# Changed 'password' to '_password' because Pg6.3 reserves the password word
-#       bmccane@maxbaud.net     98-apr-3
-#
-# use conf/shells and dbdef username length ivan@sisd.com 98-jul-13
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI);
-use FS::UID qw(cgisuidsetup getotaker);
-use FS::Record qw(qsearch qsearchs);
-use FS::svc_acct qw(fields);
-
-my($shells)="/var/spool/freeside/conf/shells";
-open(SHELLS,$shells) or die "Can't open $shells: $!";
-my(@shells)=map {
-  /^([\/\w]*)$/ or die "Illegal shell in conf/shells!";
-  $1;
-} grep $_ !~ /^#/, <SHELLS>;
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-
-my($action,$svcnum,$svc_acct,$pkgnum,$svcpart,$part_svc);
-
-if ( $QUERY_STRING =~ /^(\d+)$/ ) { #editing
-
-  $svcnum=$1;
-  $svc_acct=qsearchs('svc_acct',{'svcnum'=>$svcnum})
-    or die "Unknown (svc_acct) svcnum!";
-
-  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
-    or die "Unknown (cust_svc) svcnum!";
-
-  $pkgnum=$cust_svc->pkgnum;
-  $svcpart=$cust_svc->svcpart;
-
-  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-  die "No part_svc entry!" unless $part_svc;
-
-  $action="Edit";
-
-} else { #adding
-
-  $svc_acct=create FS::svc_acct({}); 
-
-  foreach $_ (split(/-/,$QUERY_STRING)) {
-    $pkgnum=$1 if /^pkgnum(\d+)$/;
-    $svcpart=$1 if /^svcpart(\d+)$/;
-  }
-  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-  die "No part_svc entry!" unless $part_svc;
-
-  $svcnum='';
-
-  #set gecos
-  my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-  if ($cust_pkg) {
-    my($cust_main)=qsearchs('cust_main',{'custnum'=> $cust_pkg->custnum } );
-    $svc_acct->setfield('finger',
-      $cust_main->getfield('first') . " " . $cust_main->getfield('last')
-    ) ;
-  }
-
-  #set fixed and default fields from part_svc
-  my($field);
-  foreach $field ( fields('svc_acct') ) {
-    if ( $part_svc->getfield('svc_acct__'. $field. '_flag') ne '' ) {
-      $svc_acct->setfield($field,$part_svc->getfield('svc_acct__'. $field) );
-    }
-  }
-
-  $action="Add";
-
-}
-
-my($svc)=$part_svc->getfield('svc');
-
-my($otaker)=getotaker;
-
-my($username,$password)=(
-  $svc_acct->username,
-  $svc_acct->_password ? "*HIDDEN*" : '',
-);
-
-my($ulen)=$svc_acct->dbdef_table->column('username')->length;
-my($ulen2)=$ulen+2;
-
-SendHeaders();
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>$action $svc account</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>$action $svc account</H1>
-    </CENTER><HR>
-    <FORM ACTION="process/svc_acct.cgi" METHOD=POST>
-      <INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">
-      <INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
-      <INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">
-Username: 
-<INPUT TYPE="text" NAME="username" VALUE="$username" SIZE=$ulen2 MAXLENGTH=$ulen>
-<BR>Password: 
-<INPUT TYPE="text" NAME="_password" VALUE="$password" SIZE=10 MAXLENGTH=8> 
-(blank to generate)
-END
-
-#pop
-my($popnum)=$svc_acct->popnum || 0;
-if ( $part_svc->svc_acct__popnum_flag eq "F" ) {
-  print qq!<INPUT TYPE="hidden" NAME="popnum" VALUE="$popnum">!;
-} else { 
-  print qq!<BR>POP: <SELECT NAME="popnum" SIZE=1><OPTION>\n!;
-  my($svc_acct_pop);
-  foreach $svc_acct_pop ( qsearch ('svc_acct_pop',{} ) ) {
-  print "<OPTION", $svc_acct_pop->popnum == $popnum ? ' SELECTED' : '', ">", 
-        $svc_acct_pop->popnum, ": ", 
-        $svc_acct_pop->city, ", ",
-        $svc_acct_pop->state,
-        "(", $svc_acct_pop->ac, ")/",
-        $svc_acct_pop->exch, "\n"
-      ;
-  }
-  print "</SELECT>";
-}
-
-my($uid,$gid,$finger,$dir)=(
-  $svc_acct->uid,
-  $svc_acct->gid,
-  $svc_acct->finger,
-  $svc_acct->dir,
-);
-
-print <<END;
-<INPUT TYPE="hidden" NAME="uid" VALUE="$uid">
-<INPUT TYPE="hidden" NAME="gid" VALUE="$gid">
-<BR>GECOS: <INPUT TYPE="text" NAME="finger" VALUE="$finger">
-<INPUT TYPE="hidden" NAME="dir" VALUE="$dir">
-END
-
-my($shell)=$svc_acct->shell;
-if ( $part_svc->svc_acct__shell_flag eq "F" ) {
-  print qq!<INPUT TYPE="hidden" NAME="shell" VALUE="$shell">!;
-} else {
-  print qq!<BR>Shell: <SELECT NAME="shell" SIZE=1>!;
-  my($etc_shell);
-  foreach $etc_shell (@shells) {
-    print "<OPTION", $etc_shell eq $shell ? ' SELECTED' : '', ">",
-          $etc_shell, "\n";
-  }
-  print "</SELECT>";
-}
-
-my($quota,$slipip)=(
-  $svc_acct->quota,
-  $svc_acct->slipip,
-);
-
-print qq!<INPUT TYPE="hidden" NAME="quota" VALUE="$quota">!;
-
-if ( $part_svc->svc_acct__slipip_flag eq "F" ) {
-  print qq!<INPUT TYPE="hidden" NAME="slipip" VALUE="$slipip">!;
-} else {
-  print qq!<BR>IP: <INPUT TYPE="text" NAME="slipip" VALUE="$slipip">!;
-}
-
-#submit
-print qq!<P><CENTER><INPUT TYPE="submit" VALUE="Submit"></CENTER>!; 
-
-print <<END;
-    </FORM>
-  </BODY>
-</HTML>
-END
-
-
diff --git a/htdocs/edit/svc_acct_pop.cgi b/htdocs/edit/svc_acct_pop.cgi
deleted file mode 100755 (executable)
index 46d803f..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# svc_acct_pop.cgi: Add/Edit pop (output form)
-#
-# ivan@sisd.com 98-mar-8 
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::svc_acct_pop;
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-my($svc_acct_pop,$action);
-if ( $cgi->var('QUERY_STRING') =~ /^(\d+)$/ ) { #editing
-  $svc_acct_pop=qsearchs('svc_acct_pop',{'popnum'=>$1});
-  $action='Edit';
-} else { #adding
-  $svc_acct_pop=create FS::svc_acct_pop {};
-  $action='Add';
-}
-my($hashref)=$svc_acct_pop->hashref;
-
-print header("$action POP", menubar(
-  'Main Menu' => '../',
-  'View all POPs' => "../browse/svc_acct_pop.cgi",
-)), <<END;
-    <FORM ACTION="process/svc_acct_pop.cgi" METHOD=POST>
-END
-
-#display
-
-print qq!<INPUT TYPE="hidden" NAME="popnum" VALUE="$hashref->{popnum}">!,
-      "POP #", $hashref->{popnum} ? $hashref->{popnum} : "(NEW)";
-
-print <<END;
-<PRE>
-City      <INPUT TYPE="text" NAME="city" SIZE=32 VALUE="$hashref->{city}">
-State     <INPUT TYPE="text" NAME="state" SIZE=3 MAXLENGTH=2 VALUE="$hashref->{state}">
-Area Code <INPUT TYPE="text" NAME="ac" SIZE=4 MAXLENGTH=3 VALUE="$hashref->{ac}">
-Exchange  <INPUT TYPE="text" NAME="exch" SIZE=4 MAXLENGTH=3 VALUE="$hashref->{exch}">
-</PRE>
-END
-
-print qq!<BR><INPUT TYPE="submit" VALUE="!,
-      $hashref->{popnum} ? "Apply changes" : "Add POP",
-      qq!">!;
-
-print <<END;
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/svc_acct_sm.cgi b/htdocs/edit/svc_acct_sm.cgi
deleted file mode 100755 (executable)
index 45a8eb8..0000000
+++ /dev/null
@@ -1,219 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# svc_acct_sm.cgi: Add/edit a mail alias (output form)
-#
-# Usage: svc_acct_sm.cgi {svcnum} | pkgnum{pkgnum}-svcpart{svcpart}
-#        http://server.name/path/svc_acct_sm.cgi? {svcnum} | pkgnum{pkgnum}-svcpart{svcpart}
-#
-# use {svcnum} for edit, pkgnum{pkgnum}-svcpart{svcpart} for add
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# should error out in a more CGI-friendly way, and should have more error checking (sigh).
-#
-# ivan@voicenet.com 97-jan-5
-#
-# added debugging code; fixed CPU-sucking problem with trying to edit an (unaudited) mail alias (no pkgnum)
-#
-# ivan@voicenet.com 97-may-7
-#
-# fixed uid selection
-# ivan@voicenet.com 97-jun-4
-#
-# uid selection across _CUSTOMER_, not just _PACKAGE_
-#
-# ( i need to be rewritten with fast searches)
-#
-# ivan@voicenet.com 97-oct-3
-#
-# added fast searches in some of the places where it is sorely needed...
-# I see DBI::mysql in your future...
-# ivan@voicenet.com 97-oct-23
-#
-# rewrite ivan@sisd.com 98-mar-15
-#
-# /var/spool/freeside/conf/domain ivan@sisd.com 98-jul-26
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::svc_acct_sm qw(fields);
-
-my($conf_domain)="/var/spool/freeside/conf/domain";
-open(DOMAIN,$conf_domain) or die "Can't open $conf_domain: $!";
-my($mydomain)=map {
-  /^(.*)$/ or die "Illegal line in $conf_domain!"; #yes, we trust the file
-  $1
-} grep $_ !~ /^(#|$)/, <DOMAIN>;
-close DOMAIN;
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-
-my($action,$svcnum,$svc_acct_sm,$pkgnum,$svcpart,$part_svc);
-if ( $QUERY_STRING =~ /^(\d+)$/ ) { #editing
-
-  $svcnum=$1;
-  $svc_acct_sm=qsearchs('svc_acct_sm',{'svcnum'=>$svcnum})
-    or die "Unknown (svc_acct_sm) svcnum!";
-
-  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
-    or die "Unknown (cust_svc) svcnum!";
-
-  $pkgnum=$cust_svc->pkgnum;
-  $svcpart=$cust_svc->svcpart;
-  
-  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-  die "No part_svc entry!" unless $part_svc;
-
-  $action="Edit";
-
-} else { #adding
-
-  $svc_acct_sm=create FS::svc_acct_sm({});
-
-  foreach $_ (split(/-/,$QUERY_STRING)) { #get & untaint pkgnum & svcpart
-    $pkgnum=$1 if /^pkgnum(\d+)$/;
-    $svcpart=$1 if /^svcpart(\d+)$/;
-  }
-  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-  die "No part_svc entry!" unless $part_svc;
-
-  $svcnum='';
-
-  #set fixed and default fields from part_svc
-  my($field);
-  foreach $field ( fields('svc_acct_sm') ) {
-    if ( $part_svc->getfield('svc_acct_sm__'. $field. '_flag') ne '' ) {
-      $svc_acct_sm->setfield($field,$part_svc->getfield('svc_acct_sm__'. $field) );
-    }
-  }
-
-  $action='Add';
-
-}
-
-my(%username,%domain);
-if ($pkgnum) {
-
-  #find all possible uids (and usernames)
-
-  my($u_part_svc,@u_acct_svcparts);
-  foreach $u_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_acct'}) ) {
-    push @u_acct_svcparts,$u_part_svc->getfield('svcpart');
-  }
-
-  my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-  my($custnum)=$cust_pkg->getfield('custnum');
-  my($i_cust_pkg);
-  foreach $i_cust_pkg ( qsearch('cust_pkg',{'custnum'=>$custnum}) ) {
-    my($cust_pkgnum)=$i_cust_pkg->getfield('pkgnum');
-    my($acct_svcpart);
-    foreach $acct_svcpart (@u_acct_svcparts) {   #now find the corresponding 
-                                              #record(s) in cust_svc ( for this
-                                              #pkgnum ! )
-      my($i_cust_svc);
-      foreach $i_cust_svc ( qsearch('cust_svc',{'pkgnum'=>$cust_pkgnum,'svcpart'=>$acct_svcpart}) ) {
-        my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$i_cust_svc->getfield('svcnum')});
-        $username{$svc_acct->getfield('uid')}=$svc_acct->getfield('username');
-      }  
-    }
-  }
-
-  #find all possible domains (and domsvc's)
-
-  my($d_part_svc,@d_acct_svcparts);
-  foreach $d_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_domain'}) ) {
-    push @d_acct_svcparts,$d_part_svc->getfield('svcpart');
-  }
-
-  foreach $i_cust_pkg ( qsearch('cust_pkg',{'custnum'=>$custnum}) ) {
-    my($cust_pkgnum)=$i_cust_pkg->getfield('pkgnum');
-    my($acct_svcpart);
-    foreach $acct_svcpart (@d_acct_svcparts) {
-      my($i_cust_svc);
-      foreach $i_cust_svc ( qsearch('cust_svc',{'pkgnum'=>$cust_pkgnum,'svcpart'=>$acct_svcpart}) ) {
-        my($svc_domain)=qsearch('svc_domain',{'svcnum'=>$i_cust_svc->getfield('svcnum')});
-        $domain{$svc_domain->getfield('svcnum')}=$svc_domain->getfield('domain');
-      }
-    }
-  }
-
-} elsif ( $action eq 'Edit' ) {
-
-  my($svc_acct)=qsearchs('svc_acct',{'uid'=>$svc_acct_sm->domuid});
-  $username{$svc_acct_sm->uid} = $svc_acct->username;
-
-  my($svc_domain)=qsearchs('svc_domain',{'svcnum'=>$svc_acct_sm->domsvc});
-  $domain{$svc_acct_sm->domsvc} = $svc_domain->domain;
-
-} else {
-  die "\$action eq Add, but \$pkgnum is null!\n";
-}
-
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Mail Alias $action</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Mail Alias $action</H1>
-    </CENTER>
-    <FORM ACTION="process/svc_acct_sm.cgi" METHOD=POST>
-END
-
-#display
-
-       #formatting
-       print "<PRE>";
-
-#svcnum
-print qq!<INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">!;
-print qq!Service #<FONT SIZE=+1><B>!, $svcnum ? $svcnum : " (NEW)", "</B></FONT>";
-
-#pkgnum
-print qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!;
-#svcpart
-print qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
-
-my($domuser,$domsvc,$domuid)=(
-  $svc_acct_sm->domuser,
-  $svc_acct_sm->domsvc,
-  $svc_acct_sm->domuid,
-);
-
-#domuser
-print qq!\n\nMail to <INPUT TYPE="text" NAME="domuser" VALUE="$domuser"> <I>( * for anything )</I>!;
-
-#domsvc
-print qq! \@ <SELECT NAME="domsvc" SIZE=1>!;
-foreach $_ (keys %domain) {
-  print "<OPTION", $_ eq $domsvc ? " SELECTED" : "", ">$_: $domain{$_}";
-}
-print "</SELECT>";
-
-#uid
-print qq!\nforwards to <SELECT NAME="domuid" SIZE=1>!;
-foreach $_ (keys %username) {
-  print "<OPTION", ($_ eq $domuid) ? " SELECTED" : "", ">$_: $username{$_}";
-}
-print "</SELECT>\@$mydomain mailbox.";
-
-       #formatting
-       print "</PRE>\n";
-
-print qq!<CENTER><INPUT TYPE="submit" VALUE="Submit"></CENTER>!;
-
-print <<END;
-
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/edit/svc_domain.cgi b/htdocs/edit/svc_domain.cgi
deleted file mode 100755 (executable)
index 0717a2c..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# svc_domain.cgi: Add domain (output form)
-#
-# Usage: svc_domain.cgi pkgnum{pkgnum}-svcpart{svcpart}
-#        http://server.name/path/svc_domain.cgi?pkgnum{pkgnum}-svcpart{svcpart}
-#
-# Note: Should be run setuid freeside as user nobody
-#
-# ivan@voicenet.com 97-jan-5 -> 97-jan-6
-#
-# changes for domain template 3.5
-# ivan@voicenet.com 97-jul-24
-#
-# rewrite ivan@sisd.com 98-mar-14
-#
-# no GOV in instructions ivan@sisd.com 98-jul-17
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI);
-use FS::UID qw(cgisuidsetup getotaker);
-use FS::Record qw(qsearch qsearchs);
-use FS::svc_domain qw(fields);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-
-my($action,$svcnum,$svc_domain,$pkgnum,$svcpart,$part_svc);
-
-if ( $QUERY_STRING =~ /^(\d+)$/ ) { #editing
-
-  $svcnum=$1;
-  $svc_domain=qsearchs('svc_domain',{'svcnum'=>$svcnum})
-    or die "Unknown (svc_domain) svcnum!";
-
-  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
-    or die "Unknown (cust_svc) svcnum!";
-
-  $pkgnum=$cust_svc->pkgnum;
-  $svcpart=$cust_svc->svcpart;
-
-  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-  die "No part_svc entry!" unless $part_svc;
-
-  $action="Edit";
-
-} else { #adding
-
-  $svc_domain=create FS::svc_domain({});
-  
-  foreach $_ (split(/-/,$QUERY_STRING)) {
-    $pkgnum=$1 if /^pkgnum(\d+)$/;
-    $svcpart=$1 if /^svcpart(\d+)$/;
-  }
-  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-  die "No part_svc entry!" unless $part_svc;
-
-  $svcnum='';
-
-  #set fixed and default fields from part_svc
-  my($field);
-  foreach $field ( fields('svc_domain') ) {
-    if ( $part_svc->getfield('svc_domain__'. $field. '_flag') ne '' ) {
-      $svc_domain->setfield($field,$part_svc->getfield('svc_domain__'. $field) );
-    }
-  }
-
-  $action="Add";
-
-}
-
-my($svc)=$part_svc->getfield('svc');
-
-my($otaker)=getotaker;
-
-my($domain)=(
-  $svc_domain->domain,
-);
-
-SendHeaders();
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>$action $svc</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>$action $svc</H1>
-    </CENTER><HR>
-    <FORM ACTION="process/svc_domain.cgi" METHOD=POST>
-      <INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">
-      <INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
-      <INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">
-      <INPUT TYPE="radio" NAME="action" VALUE="N">New
-      <BR><INPUT TYPE="radio" NAME="action" VALUE="M">Transfer
-
-<P>Customer agrees to be bound by NSI's
-<A HREF="http://rs.internic.net/help/agreement.txt">
-Domain Name Registration Agreement</A>
-<SELECT NAME="legal" SIZE=1><OPTION SELECTED>No<OPTION>Yes</SELECT>
-<P>Domain <INPUT TYPE="text" NAME="domain" VALUE="$domain" SIZE=28 MAXLENGTH=26>
-<BR>Purpose/Description: <INPUT TYPE="text" NAME="purpose" VALUE="" SIZE=64>
-<P><CENTER><INPUT TYPE="submit" VALUE="Submit"></CENTER>
-<UL>
-  <LI>COM is for commercial, for-profit organziations
-  <LI>ORG is for miscellaneous, usually, non-profit organizations
-  <LI>NET is for network infrastructure machines and organizations
-  <LI>EDU is for 4-year, degree granting institutions
-<!--  <LI>GOV is for United States federal government agencies
-!-->
-</UL>
-US state and local government agencies, schools, libraries, museums, and individuals should register under the US domain.  See RFC 1480 for a complete description of the US domain
-and registration procedures.
-<P>GOV registrations are limited to top-level US Federal Government agencies (see RFC 1816).
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/images/mid-logo.gif b/htdocs/images/mid-logo.gif
deleted file mode 100644 (file)
index 4ceb3ad..0000000
Binary files a/htdocs/images/mid-logo.gif and /dev/null differ
diff --git a/htdocs/images/sisd.jpg b/htdocs/images/sisd.jpg
deleted file mode 100755 (executable)
index 908a5ea..0000000
Binary files a/htdocs/images/sisd.jpg and /dev/null differ
diff --git a/htdocs/images/small-logo.gif b/htdocs/images/small-logo.gif
deleted file mode 100644 (file)
index a8e9c57..0000000
Binary files a/htdocs/images/small-logo.gif and /dev/null differ
diff --git a/htdocs/index.html b/htdocs/index.html
deleted file mode 100755 (executable)
index de0667e..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-<HTML>
-  <HEAD>
-    <TITLE>
-      Freeside Main Menu
-    </TITLE>
-  </HEAD>
-  <BODY BGCOLOR="#FFFFFF">
-  <table>
-    <tr><td>
-    <P ALIGN=CENTER>
-        <IMG BORDER=0 ALT="Silicon Interactive Software Design" SRC="images/small-logo.gif">
-    </td><td>
-      <center><font color="#ff0000" size=7>freeside main menu</font></center>
-    </td></tr>
-  </table>
-      <A HREF="http://www.sisd.com/freeside">
-        Information
-      </A>
-      <BR><A HREF="docs/">
-        Documentation
-      </A>
-    </P>
-    <HR>
-      <H3><A HREF="edit/cust_main.cgi">New Customer</A></H3>
-        <A NAME="search"><H3>Search</H3></A>
-        <MENU>
-        <LI><A HREF="search/cust_main.html">
-            customers (by last name and/or company)
-        </A>
-        <LI><A HREF="search/cust_main-payinfo.html">customers (by credit card number)</A>
-        <LI><A HREF="search/svc_acct.html">accounts (by username)</A>
-        <LI><A HREF="search/svc_domain.html">domains (by domain)</A>
-        <LI><A HREF="search/svc_acct_sm.html">mail aliases (by domain, and optionally username)</A>
-        <LI><A HREF="search/cust_bill.html">invoices (by invoice number)</A>
-        </MENU>
-        <A NAME="browse"><H3>Browse</H3></A>
-        <MENU>
-          <LI><A HREF="search/cust_main.cgi?custnum">customers (by customer number)</A>
-          <LI><A HREF="search/cust_main.cgi?last">customers (by last name)</A>
-          <LI><A HREF="search/cust_main.cgi?company">customers (by company)</A>
-          <LI><A HREF="search/cust_pkg.cgi?pkgnum">packages (by package number)</A>
-          <LI><A HREF="search/cust_pkg.cgi?APKG_pkgnum">packages with unconfigured services (by package number)</A>
-        <LI><A HREF="search/svc_acct.cgi?svcnum">accounts (by service number)</A>
-          <LI><A HREF="search/svc_acct.cgi?username">accounts (by username)</A>
-          <LI><A HREF="search/svc_acct.cgi?uid">accounts (by uid)</A>
-          <LI><A HREF="search/svc_acct.cgi?UN_svcnum">unlinked accounts (by service number)</A>
-          <LI><A HREF="search/svc_acct.cgi?UN_username">unlinked accounts (by username)</A>
-          <LI><A HREF="search/svc_acct.cgi?UN_uid">unlinked accounts (by uid)</A>
-          <LI><A HREF="search/svc_domain.cgi?svcnum">domains (by service number)</A>
-          <LI><A HREF="search/svc_domain.cgi?domain">domains (by domain)</A>
-          <LI><A HREF="search/svc_domain.cgi?UN_svcnum">unlinked domains (by service number)</A>
-          <LI><A HREF="search/svc_domain.cgi?UN_domain">unlinked domains (by domain)</A>
-      </MENU>
-          <A NAME="admin"><H3>Administration</H3></a>
-        <MENU>
-          <LI><A HREF="browse/part_svc.cgi">
-            View/Edit services
-          </A>
-            - Services are items you offer to your customers.
-          <LI><A HREF="browse/part_pkg.cgi">
-            View/Edit packages
-          </A>
-            - One or more services are grouped together into a package and
-              given pricing information.  Customers purchase packages, not
-              services.
-          <LI><A HREF="browse/agent_type.cgi">
-            View/Edit agent types
-          </A>
-            - Agent types define groups of packages that you can then assign
-              to particular agents.
-          <LI><A HREF="browse/agent.cgi">
-            View/Edit agents
-          </A>
-            - Agents are resellers of your service.  Agents may be limited
-              to a subset of your full offerings (via their agent type).
-          <BR>
-          <LI><A HREF="browse/part_referral.cgi">
-            View/Edit referrals
-          </A>
-            - Where a customer heard about your service.  Tracked for
-              informational purposes.
-          <BR>
-          <LI><A HREF="browse/cust_main_county.cgi">
-            View/Edit locales and tax rates
-          </A>
-            - Change tax rates by state, or break down a state into counties
-              and assign different tax rates to each county.
-          <BR>
-          <LI><A HREF="browse/svc_acct_pop.cgi">
-            View/Edit POPs 
-          </A>
-            - Points of Presence 
-    </MENU>
-    </FONT>
-  </BODY>
-</HTML>
diff --git a/htdocs/misc/bill.cgi b/htdocs/misc/bill.cgi
deleted file mode 100755 (executable)
index d41f6d1..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# s/FS:Search/FS::Record/ and cgisuidsetup($cgi) ivan@sisd.com 98-mar-13
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI);
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::Bill;
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-
-#untaint custnum
-$QUERY_STRING =~ /^(\d*)$/;
-my($custnum)=$1;
-my($cust_main)=qsearchs('cust_main',{'custnum'=>$custnum});
-die "Can't find customer!\n" unless $cust_main;
-
-# ? 
-bless($cust_main,"FS::Bill");
-
-my($error);
-
-$error = $cust_main->bill(
-#                          'time'=>$time
-                         );
-&idiot($error) if $error;
-
-$error = $cust_main->collect(
-#                             'invoice-time'=>$time,
-#                             'batch_card'=> 'yes',
-                             'batch_card'=> 'no',
-                             'report_badcard'=> 'yes',
-                            );
-&idiot($error) if $error;
-
-$cgi->redirect("../view/cust_main.cgi?$custnum#history");
-
-sub idiot {
-  my($error)=@_;
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error billing customer</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error billing customer</H4>
-    </CENTER>
-    Your update did not occur because of the following error:
-    <P><B>$error</B>
-  </BODY>
-</HTML>
-END
-
-  exit;
-
-}
-
diff --git a/htdocs/misc/cancel-unaudited.cgi b/htdocs/misc/cancel-unaudited.cgi
deleted file mode 100755 (executable)
index 929274f..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cancel-unaudited.cgi: Cancel an unaudited account
-#
-# Usage: cancel-unaudited.cgi svcnum
-#        http://server.name/path/cancel-unaudited.cgi pkgnum
-#
-# Note: Should be run setuid freeside as user nobody
-#
-# ivan@voicenet.com 97-apr-23
-#
-# rewrote for new API
-# ivan@voicenet.com 97-jul-21
-#
-# Search->Record, cgisuidsetup($cgi) ivan@sids.com 98-mar-19
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI); # CGI module
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::cust_svc;
-use FS::svc_acct;
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-#untaint svcnum
-$QUERY_STRING =~ /^(\d+)$/;
-my($svcnum)=$1;
-
-my($svc_acct) = qsearchs('svc_acct',{'svcnum'=>$svcnum});
-&idiot("Unknown svcnum!") unless $svc_acct;
-
-my($cust_svc) = qsearchs('cust_svc',{'svcnum'=>$svcnum});
-&idiot(qq!This account has already been audited.  Cancel the 
-    <A HREF="../view/cust_pkg.cgi?! . $cust_svc->getfield('pkgnum') .
-    qq!pkgnum"> package</A> instead.!) 
-  if $cust_svc->getfield('pkgnum') ne '';
-
-local $SIG{HUP} = 'IGNORE';
-local $SIG{INT} = 'IGNORE';
-local $SIG{QUIT} = 'IGNORE';
-local $SIG{TERM} = 'IGNORE';
-local $SIG{TSTP} = 'IGNORE';
-
-my($error);
-
-bless($svc_acct,"FS::svc_acct");
-$error = $svc_acct->cancel;
-&idiot($error) if $error;
-$error = $svc_acct->delete;
-&idiot($error) if $error;
-
-bless($cust_svc,"FS::cust_svc");
-$error = $cust_svc->delete;
-&idiot($error) if $error;
-
-$cgi->redirect("../");
-
-sub idiot {
-  my($error)=@_;
-  SendHeaders();
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error cancelling account</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Error cancelling account</H1>
-    </CENTER>
-    <HR>
-    There has been an error cancelling this acocunt:  $error
-  </BODY>
-  </HEAD>
-</HTML>
-END
-  exit;
-}
-
diff --git a/htdocs/misc/cancel_pkg.cgi b/htdocs/misc/cancel_pkg.cgi
deleted file mode 100755 (executable)
index 6702a03..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cancel_pkg.cgi: Cancel a package
-#
-# Usage: cancel_pkg.cgi pkgnum
-#        http://server.name/path/cancel_pkg.cgi pkgnum
-#
-# Note: Should be run setuid freeside as user nobody
-#
-# IT DOESN'T RUN THE APPROPRIATE PROGRAMS YET!!!!
-#
-# probably should generalize this to do cancels, suspensions, unsuspensions, etc.
-#
-# ivan@voicenet.com 97-jan-2
-#
-# still kludgy, but now runs /dbin/cancel $pkgnum
-# ivan@voicenet.com 97-feb-27
-#
-# doesn't run if pkgnum doesn't match regex
-# ivan@voicenet.com 97-mar-6
-#
-# now redirects to enter comments
-# ivan@voicenet.com 97-may-8
-#
-# rewrote for new API
-# ivan@voicenet.com 97-jul-21
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI); # CGI module
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::cust_pkg;
-use FS::CGI qw(idiot);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-#untaint pkgnum
-$QUERY_STRING =~ /^(\d+)$/ || die "Illegal pkgnum";
-my($pkgnum)=$1;
-
-my($cust_pkg) = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-
-bless($cust_pkg,'FS::cust_pkg');
-my($error)=$cust_pkg->cancel;
-idiot($error) if $error;
-
-$cgi->redirect("../view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
-
diff --git a/htdocs/misc/expire_pkg.cgi b/htdocs/misc/expire_pkg.cgi
deleted file mode 100755 (executable)
index 1635166..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# expire_pkg.cgi: Expire a package
-#
-# Usage: post form to:
-#        http://server.name/path/expire_pkg.cgi
-#
-# Note: Should be run setuid freeside as user nobody
-#
-# based on susp_pkg
-# ivan@voicenet.com 97-jul-29
-#
-# ivan@sisd.com 98-mar-17 FS::Search->FS::Record
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use Date::Parse;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::cust_pkg;
-
-my($req) = new CGI::Request;
-&cgisuidsetup($req->cgi);
-
-#untaint date & pkgnum
-
-my($date);
-if ( $req->param('date') ) {
-  str2time($req->param('date')) =~ /^(\d+)$/ or die "Illegal date";
-  $date=$1;
-} else {
-  $date='';
-}
-
-$req->param('pkgnum') =~ /^(\d+)$/ or die "Illegal pkgnum";
-my($pkgnum)=$1;
-
-my($cust_pkg) = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-my(%hash)=$cust_pkg->hash;
-$hash{expire}=$date;
-my($new)=create FS::cust_pkg ( \%hash );
-my($error) = $new->replace($cust_pkg);
-&idiot($error) if $error;
-
-$req->cgi->redirect("../view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
-
-sub idiot {
-  my($error)=@_;
-  SendHeaders();
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error expiring package</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Error expiring package</H1>
-    </CENTER>
-    <HR>
-    There has been an error expiring this package:  $error
-  </BODY>
-  </HEAD>
-</HTML>
-END
-  exit;
-}
-
diff --git a/htdocs/misc/link.cgi b/htdocs/misc/link.cgi
deleted file mode 100755 (executable)
index d1db000..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# link: instead of adding a new account, link to an existing. (output form)
-#
-# Note: Should be run setuid freeside as user nobody
-#
-# ivan@voicenet.com 97-feb-5
-#
-# rewrite ivan@sisd.com 98-mar-17
-#
-# can also link on some other fields now (about time) ivan@sisd.com 98-jun-24
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-
-my(%link_field)=(
-  'svc_acct'    => 'username',
-  'svc_domain'  => 'domain',
-  'svc_acct_sm' => '',
-  'svc_charge'  => '',
-  'svc_wo'      => '',
-);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-cgisuidsetup($cgi);
-
-my($pkgnum,$svcpart);
-foreach $_ (split(/-/,$QUERY_STRING)) { #get & untaint pkgnum & svcpart
-  $pkgnum=$1 if /^pkgnum(\d+)$/;
-  $svcpart=$1 if /^svcpart(\d+)$/;
-}
-
-my($part_svc) = qsearchs('part_svc',{'svcpart'=>$svcpart});
-my($svc) = $part_svc->getfield('svc');
-my($svcdb) = $part_svc->getfield('svcdb');
-my($link_field) = $link_field{$svcdb};
-
-CGI::Base::SendHeaders();
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Link to existing $svc account</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Link to existing $svc account</H1>
-    </CENTER><HR>
-    <FORM ACTION="process/link.cgi" METHOD=POST>
-END
-
-if ( $link_field ) { 
-  print <<END;
-  <INPUT TYPE="hidden" NAME="svcnum" VALUE="">
-  <INPUT TYPE="hidden" NAME="link_field" VALUE="$link_field">
-  $link_field of existing service: <INPUT TYPE="text" NAME="link_value">
-END
-} else {
-  print qq!Service # of existing service: <INPUT TYPE="text" NAME="svcnum" VALUE="">!;
-}
-
-print <<END;
-<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
-<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">
-<P><CENTER><INPUT TYPE="submit" VALUE="Link"></CENTER>
-    </FORM>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/misc/print-invoice.cgi b/htdocs/misc/print-invoice.cgi
deleted file mode 100755 (executable)
index 084dcc1..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# just a kludge for now, since this duplicates in a way it shouldn't stuff from
-# Bill.pm (like $lpr) ivan@sisd.com 98-jun-16
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI);
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::Invoice;
-
-my($lpr) = "|lpr -h";
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-
-#untaint invnum
-$QUERY_STRING =~ /^(\d*)$/;
-my($invnum)=$1;
-my($cust_bill)=qsearchs('cust_bill',{'invnum'=>$invnum});
-die "Can't find invoice!\n" unless $cust_bill;
-
-        bless($cust_bill,"FS::Invoice");
-        open(LPR,$lpr) or die "Can't open $lpr: $!";
-        print LPR $cust_bill->print_text; #( date )
-        close LPR
-          or die $! ? "Error closing $lpr: $!"
-                       : "Exit status $? from $lpr";
-
-my($custnum)=$cust_bill->getfield('custnum');
-
-$cgi->redirect("../view/cust_main.cgi?$custnum#history");
-
-sub idiot {
-  my($error)=@_;
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error printing invoice</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error printing invoice</H4>
-    </CENTER>
-    Your update did not occur because of the following error:
-    <P><B>$error</B>
-  </BODY>
-</HTML>
-END
-
-  exit;
-
-}
-
diff --git a/htdocs/misc/process/link.cgi b/htdocs/misc/process/link.cgi
deleted file mode 100755 (executable)
index 23fb053..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/link.cgi: link to existing customer (process form)
-#
-# ivan@voicenet.com 97-feb-5
-#
-# rewrite ivan@sisd.com 98-mar-18
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# can also link on some other fields now (about time) ivan@sisd.com 98-jun-24
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::CGI qw(idiot);
-use FS::UID qw(cgisuidsetup);
-use FS::cust_svc;
-use FS::Record qw(qsearchs);
-
-my($req)=new CGI::Request; # create form object
-cgisuidsetup($req->cgi);
-
-#$req->import_names('R'); #import CGI variables into package 'R';
-
-$req->param('pkgnum') =~ /^(\d+)$/; my($pkgnum)=$1;
-$req->param('svcpart') =~ /^(\d+)$/; my($svcpart)=$1;
-
-$req->param('svcnum') =~ /^(\d*)$/; my($svcnum)=$1;
-unless ( $svcnum ) {
-  my($part_svc) = qsearchs('part_svc',{'svcpart'=>$svcpart});
-  my($svcdb) = $part_svc->getfield('svcdb');
-  $req->param('link_field') =~ /^(\w+)$/; my($link_field)=$1;
-  my($svc_acct)=qsearchs($svcdb,{$link_field => $req->param('link_value') });
-  idiot("$link_field not found!") unless $svc_acct;
-  $svcnum=$svc_acct->svcnum;
-}
-
-my($old)=qsearchs('cust_svc',{'svcnum'=>$svcnum});
-die "svcnum not found!" unless $old;
-my($new)=create FS::cust_svc ({
-  'svcnum' => $svcnum,
-  'pkgnum' => $pkgnum,
-  'svcpart' => $svcpart,
-});
-
-my($error);
-$error = $new->replace($old);
-
-unless ($error) {
-  #no errors, so let's view this customer.
-  $req->cgi->redirect("../../view/cust_pkg.cgi?$pkgnum");
-} else {
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error</H4>
-    </CENTER>
-    Your update did not occur because of the following error:
-    <P><B>$error</B>
-    <P>Hit the <I>Back</I> button in your web browser, correct this mistake, and submit the form again.
-  </BODY>
-</HTML>
-END
-}
-
diff --git a/htdocs/misc/susp_pkg.cgi b/htdocs/misc/susp_pkg.cgi
deleted file mode 100755 (executable)
index 7b23cae..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# susp_pkg.cgi: Suspend a package
-#
-# Usage: susp_pkg.cgi pkgnum
-#        http://server.name/path/susp_pkg.cgi pkgnum
-#
-# Note: Should be run setuid freeside as user nobody
-#
-# probably should generalize this to do cancels, suspensions, unsuspensions, etc.
-#
-# ivan@voicenet.com 97-feb-27
-#
-# now redirects to enter comments
-# ivan@voicenet.com 97-may-8
-#
-# rewrote for new API
-# ivan@voicenet.com 97-jul-21
-#
-# FS::Search -> FS::Record ivan@sisd.com 98-mar-17
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI); # CGI module
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::cust_pkg;
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-#untaint pkgnum
-$QUERY_STRING =~ /^(\d+)$/ || die "Illegal pkgnum";
-my($pkgnum)=$1;
-
-my($cust_pkg) = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-
-bless($cust_pkg,'FS::cust_pkg');
-my($error)=$cust_pkg->suspend;
-&idiot($error) if $error;
-
-$cgi->redirect("../view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
-
-sub idiot {
-  my($error)=@_;
-  SendHeaders();
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error suspending package</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Error suspending package</H1>
-    </CENTER>
-    <HR>
-    There has been an error suspending this package:  $error
-  </BODY>
-  </HEAD>
-</HTML>
-END
-  exit;
-}
-
diff --git a/htdocs/misc/unsusp_pkg.cgi b/htdocs/misc/unsusp_pkg.cgi
deleted file mode 100755 (executable)
index 2f340c6..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# susp_pkg.cgi: Unsuspend a package
-#
-# Usage: susp_pkg.cgi pkgnum
-#        http://server.name/path/susp_pkg.cgi pkgnum
-#
-# Note: Should be run setuid freeside as user nobody
-#
-# probably should generalize this to do cancels, suspensions, unsuspensions, etc.
-#
-# ivan@voicenet.com 97-feb-27
-#
-# now redirects to enter comments
-# ivan@voicenet.com 97-may-8
-#
-# rewrote for new API
-# ivan@voicenet.com 97-jul-21
-#
-# FS::Search -> FS::Record ivan@sisd.com 98-mar-17
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI); # CGI module
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::cust_pkg;
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-#untaint pkgnum
-$QUERY_STRING =~ /^(\d+)$/ || die "Illegal pkgnum";
-my($pkgnum)=$1;
-
-my($cust_pkg) = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-
-bless($cust_pkg,'FS::cust_pkg');
-my($error)=$cust_pkg->unsuspend;
-&idiot($error) if $error;
-
-$cgi->redirect("../view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
-
-sub idiot {
-  my($error)=@_;
-  SendHeaders();
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error unsuspending package</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Error unsuspending package</H1>
-    </CENTER>
-    <HR>
-    There has been an error unsuspending this package:  $error
-  </BODY>
-  </HEAD>
-</HTML>
-END
-  exit;
-}
-
diff --git a/htdocs/search/cust_bill.cgi b/htdocs/search/cust_bill.cgi
deleted file mode 100755 (executable)
index 5be84b7..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_bill.cgi: Search for invoices (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/cust_bill.cgi
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# ivan@voicenet.com 97-apr-4
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Request;
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-
-my($req)=new CGI::Request;
-cgisuidsetup($req->cgi);
-
-$req->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/;
-my($invnum)=$2;
-
-if ( qsearchs('cust_bill',{'invnum'=>$invnum}) ) {
-  $req->cgi->redirect("../view/cust_bill.cgi?$invnum");  #redirect
-} else { #error
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Invoice Search Error</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H3>Invoice Search Error</H3>
-    <HR>
-    Invoice not found.
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
-}
-
diff --git a/htdocs/search/cust_bill.html b/htdocs/search/cust_bill.html
deleted file mode 100755 (executable)
index 4adb40e..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<HTML>
-  <HEAD>
-    <TITLE>Invoice Search</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>Invoice Search</H1>
-    </CENTER>
-    <HR>
-    <FORM ACTION="cust_bill.cgi" METHOD="post">
-      Search for <B>invoice #</B>: 
-      <INPUT TYPE="text" NAME="invnum">
-
-      <P><INPUT TYPE="submit" VALUE="Search">
-
-    </FORM>
-
-  <HR>
-  </BODY>
-</HTML>
-
diff --git a/htdocs/search/cust_main-payinfo.html b/htdocs/search/cust_main-payinfo.html
deleted file mode 100755 (executable)
index 92341ad..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<HTML>
-  <HEAD>
-    <TITLE>Customer Search</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>Customer Search</H1>
-    </CENTER>
-    <HR>
-    <FORM ACTION="cust_main.cgi" METHOD="post">
-      Search for <B>Credit card #</B>: 
-      <INPUT TYPE="hidden" NAME="card_on" VALUE="TRUE">
-      <INPUT TYPE="text" NAME="card">
-
-      <P><INPUT TYPE="submit" VALUE="Search">
-
-    </FORM>
-    <HR>
-  </BODY>
-</HTML>
-
diff --git a/htdocs/search/cust_main.cgi b/htdocs/search/cust_main.cgi
deleted file mode 100755 (executable)
index 70ce991..0000000
+++ /dev/null
@@ -1,235 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# process/cust_main.cgi: Search for customers (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/cust_main.cgi
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# ivan@voicenet.com 96-dec-12
-#
-# rewrite ivan@sisd.com 98-mar-4
-#
-# now does browsing too ivan@sisd.com 98-mar-6
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# display total, use FS::CGI ivan@sisd.com 98-jul-17
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use IO::Handle;
-use IPC::Open2;
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header idiot);
-
-my($fuzziness)=2; #fuzziness for fuzzy searches, see man agrep
-                  #0-4: 0=no fuzz, 4=very fuzzy (too much fuzz!)
-
-my($req)=new CGI::Request;
-&cgisuidsetup($req->cgi);
-
-my(@cust_main);
-my($sortby);
-
-my($query)=$req->cgi->var('QUERY_STRING');
-if ( $query eq 'custnum' ) {
-  $sortby=\*custnum_sort;
-  @cust_main=qsearch('cust_main',{});  
-} elsif ( $query eq 'last' ) {
-  $sortby=\*last_sort;
-  @cust_main=qsearch('cust_main',{});  
-} elsif ( $query eq 'company' ) {
-  $sortby=\*company_sort;
-  @cust_main=qsearch('cust_main',{});  
-} else {
-  &cardsearch if ($req->param('card_on') );
-  &lastsearch if ($req->param('last_on') );
-  &companysearch if ($req->param('company_on') );
-}
-
-if ( scalar(@cust_main) == 1 ) {
-  $req->cgi->redirect("../view/cust_main.cgi?". $cust_main[0]->custnum);
-  exit;
-} elsif ( scalar(@cust_main) == 0 ) {
-  idiot "No matching customers found!\n";
-  exit;
-} else { 
-
-  my($total)=scalar(@cust_main);
-  CGI::Base::SendHeaders(); # one guess
-  print header("Customer Search Results",''), <<END;
-
-    $total matching customers found
-    <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
-      <TR>
-        <TH>Cust. #</TH>
-        <TH>Contact name</TH>
-        <TH>Company</TH>
-      </TR>
-END
-
-  my($lines)=16;
-  my($lcount)=$lines;
-  my(%saw,$cust_main);
-  foreach $cust_main (
-    sort $sortby grep(!$saw{$_->custnum}++, @cust_main)
-  ) {
-    my($custnum,$last,$first,$company)=(
-      $cust_main->custnum,
-      $cust_main->getfield('last'),
-      $cust_main->getfield('first'),
-      $cust_main->company,
-    );
-    print <<END;
-    <TR>
-      <TD><A HREF="../view/cust_main.cgi?$custnum"><FONT SIZE=-1>$custnum</FONT></A></TD>
-      <TD><FONT SIZE=-1>$last, $first</FONT></TD>
-      <TD><FONT SIZE=-1>$company</FONT></TD>
-    </TR>
-END
-    if ($lcount-- == 0) { # lots of little tables instead of one big one
-      $lcount=$lines;
-      print <<END;   
-  </TABLE>
-  <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
-    <TR>
-      <TH>Cust. #</TH>
-      <TH>Contact name</TH>
-      <TH>Company<TH>
-    </TR>
-END
-    }
-  }
-  print <<END;
-    </TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
-}
-
-#
-
-sub last_sort {
-  $a->getfield('last') cmp $b->getfield('last');
-}
-
-sub company_sort {
-  $a->getfield('company') cmp $b->getfield('company');
-}
-
-sub custnum_sort {
-  $a->getfield('custnum') <=> $b->getfield('custnum');
-}
-
-sub cardsearch {
-
-  my($card)=$req->param('card');
-  $card =~ s/\D//g;
-  $card =~ /^(\d{13,16})$/ or do { idiot "Illegal card number\n"; exit; };
-  my($payinfo)=$1;
-
-  push @cust_main, qsearch('cust_main',{'payinfo'=>$payinfo, 'payby'=>'CARD'});
-
-}
-
-sub lastsearch {
-  my(%last_type);
-  foreach ( $req->param('last_type') ) {
-    $last_type{$_}++;
-  }
-
-  $req->param('last_text') =~ /^([\w \,\.\-\']*)$/
-    or do { idiot "Illegal last name"; exit; };
-  my($last)=$1;
-
-  if ( $last_type{'Exact'}
-       && ! $last_type{'Fuzzy'} 
-     #  && ! $last_type{'Sound-alike'}
-  ) {
-
-    push @cust_main, qsearch('cust_main',{'last'=>$last});
-
-  } else {
-
-    my(%last);
-
-    my(@all_last)=map $_->getfield('last'), qsearch('cust_main',{});
-    if ($last_type{'Fuzzy'}) { 
-      my($reader,$writer) = ( new IO::Handle, new IO::Handle );
-      open2($reader,$writer,'agrep',"-$fuzziness",'-i','-k',
-            substr($last,0,30));
-      print $writer join("\n",@all_last),"\n";
-      close $writer;
-      while (<$reader>) {
-        chop;
-        $last{$_}++;
-      } 
-      close $reader;
-    }
-
-    #if ($last_type{'Sound-alike'}) {
-    #}
-
-    foreach ( keys %last ) {
-      push @cust_main, qsearch('cust_main',{'last'=>$_});
-    }
-
-  }
-  $sortby=\*last_sort;
-}
-
-sub companysearch {
-
-  my(%company_type);
-  foreach ( $req->param('company_type') ) {
-    $company_type{$_}++ 
-  };
-
-  $req->param('company_text') =~ /^([\w \,\.\-\']*)$/
-    or do { idiot "Illegal company"; exit; };
-  my($company)=$1;
-
-  if ( $company_type{'Exact'}
-       && ! $company_type{'Fuzzy'} 
-     #  && ! $company_type{'Sound-alike'}
-  ) {
-
-    push @cust_main, qsearch('cust_main',{'company'=>$company});
-
-  } else {
-
-    my(%company);
-    my(@all_company)=map $_->company, qsearch('cust_main',{});
-
-    if ($company_type{'Fuzzy'}) { 
-      my($reader,$writer) = ( new IO::Handle, new IO::Handle );
-      open2($reader,$writer,'agrep',"-$fuzziness",'-i','-k',
-            substr($company,0,30));
-      print $writer join("\n",@all_company),"\n";
-      close $writer;
-      while (<$reader>) {
-        chop;
-        $company{$_}++;
-      }
-      close $reader;
-    }
-
-    #if ($company_type{'Sound-alike'}) {
-    #}
-
-    foreach ( keys %company ) {
-      push @cust_main, qsearch('cust_main',{'company'=>$_});
-    }
-
-  }
-  $sortby=\*company_sort;
-
-}
diff --git a/htdocs/search/cust_main.html b/htdocs/search/cust_main.html
deleted file mode 100755 (executable)
index 656943f..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-<HTML>
-  <HEAD>
-    <TITLE>Customer Search</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>Customer Search</H1>
-    </CENTER>
-    <HR>
-    <FORM ACTION="cust_main.cgi" METHOD="post">
-      <INPUT TYPE="checkbox" NAME="last_on"> Search for <B>last name</B>: 
-      <INPUT TYPE="text" NAME="last_text">
-      using search method(s): <SELECT NAME="last_type" MULTIPLE>
-        <OPTION SELECTED>Fuzzy
-        <OPTION>Exact
-      </SELECT>
-
-      <P><INPUT TYPE="checkbox" NAME="company_on"> Search for <B>company</B>: 
-      <INPUT TYPE="text" NAME="company_text">
-      using search methods(s): <SELECT NAME="company_type" MULTIPLE>
-        <OPTION SELECTED>Fuzzy
-        <OPTION>Exact
-      </SELECT>
-
-      <P><INPUT TYPE="submit" VALUE="Search"> Note: Fuzzy searching can take a while.  Please be patient.
-
-    </FORM>
-
-  <HR>Explanation of search methods:
-  <UL>
-    <LI><B>Fuzzy</B> - Searches for matches that are close to your text.
-    <LI><B>Exact</B> - Finds exact matches only, but much faster than the other search methods.
-  </UL>
-  </BODY>
-</HTML>
-
diff --git a/htdocs/search/cust_pkg.cgi b/htdocs/search/cust_pkg.cgi
deleted file mode 100755 (executable)
index 967068f..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_pkg.cgi: search/browse for packages
-#
-# based on search/svc_acct.cgi ivan@sisd.com 98-jul-17
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header idiot);
-
-my($req)=new CGI::Request;
-&cgisuidsetup($req->cgi);
-
-my(@cust_pkg,$sortby);
-
-my($query)=$req->cgi->var('QUERY_STRING');
-#this tree is a little bit redundant
-if ( $query eq 'pkgnum' ) {
-  $sortby=\*pkgnum_sort;
-  @cust_pkg=qsearch('cust_pkg',{});
-} elsif ( $query eq 'APKG_pkgnum' ) {
-  $sortby=\*pkgnum_sort;
-
-  #perhaps this should go in cust_pkg as a qsearch-like constructor?
-  my($cust_pkg);
-  foreach $cust_pkg (qsearch('cust_pkg',{})) {
-    my($flag)=0;
-    my($pkg_svc);
-    PKG_SVC: 
-    foreach $pkg_svc (qsearch('pkg_svc',{ 'pkgpart' => $cust_pkg->pkgpart })) {
-      if ( $pkg_svc->quantity 
-           > scalar(qsearch('cust_svc',{
-               'pkgnum' => $cust_pkg->pkgnum,
-               'svcpart' => $pkg_svc->svcpart,
-             }))
-         )
-      {
-        $flag=1;
-        last PKG_SVC;
-      }
-    }
-    push @cust_pkg, $cust_pkg if $flag;
-  }
-} else {
-  die "Empty QUERY_STRING!";
-}
-
-if ( scalar(@cust_pkg) == 1 ) {
-  my($pkgnum)=$cust_pkg[0]->pkgnum;
-  $req->cgi->redirect("../view/cust_pkg.cgi?$pkgnum");
-  exit;
-} elsif ( scalar(@cust_pkg) == 0 ) { #error
-  &idiot("No packages found");
-  exit;
-} else {
-  my($total)=scalar(@cust_pkg);
-  CGI::Base::SendHeaders(); # one guess
-  print header('Package Search Results',''), <<END;
-    $total matching packages found
-    <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
-      <TR>
-        <TH>Package #</TH>
-        <TH>Customer #</TH>
-        <TH>Name</TH>
-        <TH>Company</TH>
-      </TR>
-END
-
-  my($lines)=16;
-  my($lcount)=$lines;
-  my(%saw,$cust_pkg);
-  foreach $cust_pkg (
-    sort $sortby grep(!$saw{$_->pkgnum}++, @cust_pkg)
-  ) {
-    my($cust_main)=qsearchs('cust_main',{'custnum'=>$cust_pkg->custnum});
-    my($pkgnum,$custnum,$name,$company)=(
-      $cust_pkg->pkgnum,
-      $cust_main->custnum,
-      $cust_main->last. ', '. $cust_main->first,
-      $cust_main->company,
-    );
-    print <<END;
-    <TR>
-      <TD><A HREF="../view/cust_pkg.cgi?$pkgnum"><FONT SIZE=-1>$pkgnum</FONT></A></TD>
-      <TD><FONT SIZE=-1>$custnum</FONT></TD>
-      <TD><FONT SIZE=-1>$name</FONT></TD>
-      <TD><FONT SIZE=-1>$company</FONT></TD>
-    </TR>
-END
-    if ($lcount-- == 0) { # lots of little tables instead of one big one
-      $lcount=$lines;
-      print <<END;   
-  </TABLE>
-  <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
-    <TR>
-        <TH>Package #</TH>
-        <TH>Customer #</TH>
-        <TH>Name</TH>
-        <TH>Company</TH>
-      <TH>
-    </TR>
-END
-    }
-  }
-  print <<END;
-    </TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-  exit;
-
-}
-
-sub pkgnum_sort {
-  $a->getfield('pkgnum') <=> $b->getfield('pkgnum');
-}
-
diff --git a/htdocs/search/svc_acct.cgi b/htdocs/search/svc_acct.cgi
deleted file mode 100755 (executable)
index 250a741..0000000
+++ /dev/null
@@ -1,186 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# svc_acct.cgi: Search for customers (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/svc_acct.cgi
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# loosely (sp?) based on search/cust_main.cgi
-#
-# ivan@voicenet.com 96-jan-3 -> 96-jan-4
-#
-# rewrite (now does browsing too) ivan@sisd.com 98-mar-9
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# show unlinked accounts ivan@sisd.com 98-jun-22
-#
-# use FS::CGI, show total ivan@sisd.com 98-jul-17
-#
-# give service and customer info too ivan@sisd.com 98-aug-16
-
-use strict;
-use CGI::Request; # form processing module
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header idiot);
-
-my($req)=new CGI::Request; # create form object
-&cgisuidsetup($req->cgi);
-
-my(@svc_acct,$sortby);
-
-my($query)=$req->cgi->var('QUERY_STRING');
-#this tree is a little bit redundant
-if ( $query eq 'svcnum' ) {
-  $sortby=\*svcnum_sort;
-  @svc_acct=qsearch('svc_acct',{});
-} elsif ( $query eq 'username' ) {
-  $sortby=\*username_sort;
-  @svc_acct=qsearch('svc_acct',{});
-} elsif ( $query eq 'uid' ) {
-  $sortby=\*uid_sort;
-  @svc_acct=grep $_->uid ne '', qsearch('svc_acct',{});
-} elsif ( $query eq 'UN_svcnum' ) {
-  $sortby=\*svcnum_sort;
-  @svc_acct = grep qsearchs('cust_svc',{
-      'svcnum' => $_->svcnum,
-      'pkgnum' => '',
-    }), qsearch('svc_acct',{});
-} elsif ( $query eq 'UN_username' ) {
-  $sortby=\*username_sort;
-  @svc_acct = grep qsearchs('cust_svc',{
-      'svcnum' => $_->svcnum,
-      'pkgnum' => '',
-    }), qsearch('svc_acct',{});
-} elsif ( $query eq 'UN_uid' ) {
-  $sortby=\*uid_sort;
-  @svc_acct = grep qsearchs('cust_svc',{
-      'svcnum' => $_->svcnum,
-      'pkgnum' => '',
-    }), qsearch('svc_acct',{});
-} else {
-  &usernamesearch;
-}
-
-if ( scalar(@svc_acct) == 1 ) {
-  my($svcnum)=$svc_acct[0]->svcnum;
-  $req->cgi->redirect("../view/svc_acct.cgi?$svcnum");  #redirect
-  exit;
-} elsif ( scalar(@svc_acct) == 0 ) { #error
-  idiot("Account not found");
-  exit;
-} else {
-  my($total)=scalar(@svc_acct);
-  CGI::Base::SendHeaders(); # one guess
-  print header("Account Search Results",''), <<END;
-    $total matching accounts found
-    <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
-      <TR>
-        <TH>Service #</TH>
-        <TH>Username</TH>
-        <TH>UID</TH>
-        <TH>Service</TH>
-        <TH>Customer #</TH>
-        <TH>Contact name</TH>
-        <TH>Company</TH>
-      </TR>
-END
-
-  my($lines)=16;
-  my($lcount)=$lines;
-  my(%saw,$svc_acct);
-  foreach $svc_acct (
-    sort $sortby grep(!$saw{$_->svcnum}++, @svc_acct)
-  ) {
-    my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svc_acct->svcnum })
-      or die "No cust_svc record for svcnum ". $svc_acct->svcnum;
-    my $part_svc = qsearchs('part_svc', { 'svcpart' => $cust_svc->svcpart })
-      or die "No part_svc record for svcpart ". $cust_svc->svcpart;
-    my($cust_pkg,$cust_main);
-    if ( $cust_svc->pkgnum ) {
-      $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $cust_svc->pkgnum })
-        or die "No cust_pkg record for pkgnum ". $cust_svc->pkgnum;
-      $cust_main = qsearchs('cust_main', { 'custnum' => $cust_pkg->custnum })
-        or die "No cust_main record for custnum ". $cust_pkg->custnum;
-    }
-    my($svcnum,$username,$uid,$svc,$custnum,$last,$first,$company)=(
-      $svc_acct->svcnum,
-      $svc_acct->getfield('username'),
-      $svc_acct->getfield('uid'),
-      $part_svc->svc,
-      $cust_svc->pkgnum ? $cust_main->custnum : '',
-      $cust_svc->pkgnum ? $cust_main->getfield('last') : '',
-      $cust_svc->pkgnum ? $cust_main->getfield('first') : '',
-      $cust_svc->pkgnum ? $cust_main->company : '',
-    );
-    my($pcustnum) = $custnum
-      ? "<A HREF=\"../view/cust_main.cgi?$custnum\"><FONT SIZE=-1>$custnum</FONT></A>"
-      : "<I>(unlinked)</I>"
-    ;
-    my($pname) = $custnum ? "$last, $first" : '';
-    print <<END;
-    <TR>
-      <TD><A HREF="../view/svc_acct.cgi?$svcnum"><FONT SIZE=-1>$svcnum</FONT></A></TD>
-      <TD><FONT SIZE=-1>$username</FONT></TD>
-      <TD><FONT SIZE=-1>$uid</FONT></TD>
-      <TD><FONT SIZE=-1>$svc</FONT></TH>
-      <TD><FONT SIZE=-1>$pcustnum</FONT></TH>
-      <TD><FONT SIZE=-1>$pname<FONT></TH>
-      <TD><FONT SIZE=-1>$company</FONT></TH>
-    </TR>
-END
-    if ($lcount-- == 0) { # lots of little tables instead of one big one
-      $lcount=$lines;
-      print <<END;   
-  </TABLE>
-  <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
-    <TR>
-      <TH>Service #</TH>
-      <TH>Userame</TH>
-      <TH>UID</TH>
-        <TH>Service</TH>
-        <TH>Customer #</TH>
-        <TH>Contact name</TH>
-        <TH>Company</TH>
-    </TR>
-END
-    }
-  }
-  print <<END;
-    </TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-  exit;
-
-}
-
-sub svcnum_sort {
-  $a->getfield('svcnum') <=> $b->getfield('svcnum');
-}
-
-sub username_sort {
-  $a->getfield('username') cmp $b->getfield('username');
-}
-
-sub uid_sort {
-  $a->getfield('uid') <=> $b->getfield('uid');
-}
-
-sub usernamesearch {
-
-  $req->param('username') =~ /^([\w\d\-]{2,8})$/; #untaint username_text
-  my($username)=$1;
-
-  @svc_acct=qsearch('svc_acct',{'username'=>$username});
-
-}
-
-
diff --git a/htdocs/search/svc_acct.html b/htdocs/search/svc_acct.html
deleted file mode 100755 (executable)
index 91291be..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<HTML>
-  <HEAD>
-    <TITLE>Account Search</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>Account Search</H1>
-    </CENTER>
-    <HR>
-    <FORM ACTION="svc_acct.cgi" METHOD="post">
-      Search for <B>username</B>: 
-      <INPUT TYPE="text" NAME="username">
-
-      <P><INPUT TYPE="submit" VALUE="Search">
-
-    </FORM>
-
-  <HR>
-  </BODY>
-</HTML>
-
diff --git a/htdocs/search/svc_acct_sm.cgi b/htdocs/search/svc_acct_sm.cgi
deleted file mode 100755 (executable)
index 3b1a4cf..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# svc_acct_sm.cgi: Search for domains (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/svc_domain.cgi
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# ivan@voicenet.com 96-mar-5
-#
-# need to look at table in results to make it more readable
-#
-# ivan@voicenet.com
-#
-# rewrite ivan@sisd.com 98-mar-15
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-
-my($conf_domain)="/var/spool/freeside/conf/domain";
-open(DOMAIN,$conf_domain) or die "Can't open $conf_domain: $!";
-my($mydomain)=map {
-  /^(.*)$/ or die "Illegal line in $conf_domain!"; #yes, we trust the file
-  $1
-} grep $_ !~ /^(#|$)/, <DOMAIN>;
-close DOMAIN;
-
-my($req)=new CGI::Request; # create form object
-&cgisuidsetup($req->cgi);
-
-$req->param('domuser') =~ /^([a-z0-9_\-]{0,32})$/;
-my($domuser)=$1;
-
-$req->param('domain') =~ /^([\w\-\.]+)$/ or die "Illegal domain";
-my($svc_domain)=qsearchs('svc_domain',{'domain'=>$1})
-  or die "Unknown domain";
-my($domsvc)=$svc_domain->svcnum;
-
-my(@svc_acct_sm);
-if ($domuser) {
-  @svc_acct_sm=qsearch('svc_acct_sm',{
-    'domuser' => $domuser,
-    'domsvc'  => $domsvc,
-  });
-} else {
-  @svc_acct_sm=qsearch('svc_acct_sm',{'domsvc' => $domsvc});
-}
-
-if ( scalar(@svc_acct_sm) == 1 ) {
-  my($svcnum)=$svc_acct_sm[0]->svcnum;
-  $req->cgi->redirect("../view/svc_acct_sm.cgi?$svcnum");  #redirect
-} elsif ( scalar(@svc_acct_sm) > 1 ) {
-  CGI::Base::SendHeaders();
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Mail Alias Search Results</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Mail Alias Search Results</H4>
-    <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
-      <TR>
-        <TH>Mail to<BR><FONT SIZE=-2>(click here to view mail alias)</FONT></TH>
-        <TH>Forwards to<BR><FONT SIZE=-2>(click here to view account)</FONT></TH>
-      </TR>
-END
-
-  my($svc_acct_sm);
-  foreach $svc_acct_sm (@svc_acct_sm) {
-    my($svcnum,$domuser,$domuid,$domsvc)=(
-      $svc_acct_sm->svcnum,
-      $svc_acct_sm->domuser,
-      $svc_acct_sm->domuid,
-      $svc_acct_sm->domsvc,
-    );
-    my($svc_domain)=qsearchs('svc_domain',{'svcnum'=>$domsvc});
-    my($domain)=$svc_domain->domain;
-    my($svc_acct)=qsearchs('svc_acct',{'uid'=>$domuid});
-    my($username)=$svc_acct->username;
-    my($svc_acct_svcnum)=$svc_acct->svcnum;
-
-    print <<END;
-<TR>\n        <TD> <A HREF="../view/svc_acct_sm.cgi?$svcnum">
-END
-
-    print '', ( ($domuser eq '*') ? "<I>(anything)</I>" : $domuser );
-
-    print <<END;
-\@$domain</A> </TD>\n
-<TD> <A HREF="../view/svc_acct.cgi?$svc_acct_svcnum">$username\@$mydomain</A> </TD>\n      </TR>\n
-END
-
-  }
-
-  print <<END;
-      </TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
-} else { #error
-  CGI::Base::SendHeaders(); # one guess
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Mail Alias Search Error</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H3>Mail Alias Search Error</H3>
-    <HR>
-    Mail Alias not found.
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
-}
-
diff --git a/htdocs/search/svc_acct_sm.html b/htdocs/search/svc_acct_sm.html
deleted file mode 100755 (executable)
index 0719856..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<HTML>
-  <HEAD>
-    <TITLE>Mail Alias Search</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>Mail Alias Search</H1>
-    </CENTER>
-    <HR>
-    <FORM ACTION="svc_acct_sm.cgi" METHOD="post">
-      Search for <B>mail alias</B>: 
-      <INPUT TYPE="text" NAME="domuser"><FONT SIZE=-1>(opt.)</FONT> @
-      <INPUT TYPE="text" NAME="domain"><FONT SIZE=-1>(req.)</FONT>
-
-      <P><INPUT TYPE="submit" VALUE="Search">
-
-    </FORM>
-
-  <HR>
-
-  </BODY>
-</HTML>
-
diff --git a/htdocs/search/svc_domain.cgi b/htdocs/search/svc_domain.cgi
deleted file mode 100755 (executable)
index d527703..0000000
+++ /dev/null
@@ -1,139 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# svc_domain.cgi: Search for domains (process form)
-#
-# Usage: post form to:
-#        http://server.name/path/svc_domain.cgi
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# ivan@voicenet.com 97-mar-5
-#
-# rewrite ivan@sisd.com 98-mar-14
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# display total, use FS::CGI now does browsing too ivan@sisd.com 98-jul-17
-
-use strict;
-use CGI::Request;
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-use FS::CGI qw(header idiot);
-
-my($req)=new CGI::Request;
-&cgisuidsetup($req->cgi);
-
-my(@svc_domain);
-my($sortby);
-
-my($query)=$req->cgi->var('QUERY_STRING');
-if ( $query eq 'svcnum' ) {
-  $sortby=\*svcnum_sort;
-  @svc_domain=qsearch('svc_domain',{});
-} elsif ( $query eq 'domain' ) {
-  $sortby=\*domain_sort;
-  @svc_domain=qsearch('svc_domain',{});
-} elsif ( $query eq 'UN_svcnum' ) {
-  $sortby=\*svcnum_sort;
-  @svc_domain = grep qsearchs('cust_svc',{
-      'svcnum' => $_->svcnum,
-      'pkgnum' => '',
-    }), qsearch('svc_domain',{});
-} elsif ( $query eq 'UN_domain' ) {
-  $sortby=\*domain_sort;
-  @svc_domain = grep qsearchs('cust_svc',{
-      'svcnum' => $_->svcnum,
-      'pkgnum' => '',
-    }), qsearch('svc_domain',{});
-} else {
-  $req->param('domain') =~ /^([\w\-\.]+)$/; 
-  my($domain)=$1;
-  push @svc_domain, qsearchs('svc_domain',{'domain'=>$domain});
-}
-
-if ( scalar(@svc_domain) == 1 ) {
-  $req->cgi->redirect("../view/svc_domain.cgi?". $svc_domain[0]->svcnum);
-  exit;
-} elsif ( scalar(@svc_domain) == 0 ) {
-  idiot "No matching domains found!\n";
-  exit;
-} else {
-  CGI::Base::SendHeaders(); # one guess
-
-  my($total)=scalar(@svc_domain);
-  CGI::Base::SendHeaders(); # one guess
-  print header("Domain Search Results",''), <<END;
-
-    $total matching domains found
-    <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
-      <TR>
-        <TH>Service #</TH>
-        <TH>Domain</TH>
-        <TH></TH>
-      </TR>
-END
-
-  my($lines)=16;
-  my($lcount)=$lines;
-  my(%saw,$svc_domain);
-  foreach $svc_domain (
-    sort $sortby grep(!$saw{$_->svcnum}++, @svc_domain)
-  ) {
-    my($svcnum,$domain)=(
-      $svc_domain->svcnum,
-      $svc_domain->domain,
-    );
-    my($malias);
-    if ( qsearch('svc_acct_sm',{'domsvc'=>$svcnum}) ) {
-      $malias=(
-        qq|<FORM ACTION="svc_acct_sm.cgi" METHOD="post">|.
-          qq|<INPUT TYPE="hidden" NAME="domuser" VALUE="">|.
-          qq|<INPUT TYPE="hidden" NAME="domain" VALUE="$domain">|.
-          qq|<INPUT TYPE="submit" VALUE="(mail aliases)">|.
-          qq|</FORM>|
-      );
-    } else {
-      $malias='';
-    }
-    print <<END;
-    <TR>
-      <TD><A HREF="../view/svc_domain.cgi?$svcnum"><FONT SIZE=-1>$svcnum</FONT></A></TD>
-      <TD><FONT SIZE=-1>$domain</FONT></TD>
-      <TD><FONT SIZE=-1>$malias</FONT></TD>
-    </TR>
-END
-    if ($lcount-- == 0) { # lots of little tables instead of one big one
-      $lcount=$lines;
-      print <<END;   
-  </TABLE>
-  <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
-    <TR>
-      <TH>Service #</TH>
-      <TH>Domain</TH>
-      <TH></TH>
-    </TR>
-END
-    }
-  }
-  print <<END;
-    </TABLE>
-    </CENTER>
-  </BODY>
-</HTML>
-END
-
-}
-
-sub svcnum_sort {
-  $a->getfield('svcnum') <=> $b->getfield('svcnum');
-}
-
-sub domain_sort {
-  $a->getfield('domain') cmp $b->getfield('doimain');
-}
-
-
diff --git a/htdocs/search/svc_domain.html b/htdocs/search/svc_domain.html
deleted file mode 100755 (executable)
index 533743b..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<HTML>
-  <HEAD>
-    <TITLE>Domain Search</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-      <H1>Domain Search</H1>
-    </CENTER>
-    <HR>
-    <FORM ACTION="svc_domain.cgi" METHOD="post">
-      Search for <B>domain</B>: 
-      <INPUT TYPE="text" NAME="domain">
-
-      <P><INPUT TYPE="submit" VALUE="Search">
-
-    </FORM>
-
-  <HR>
-
-  </BODY>
-</HTML>
-
diff --git a/htdocs/view/cust_bill.cgi b/htdocs/view/cust_bill.cgi
deleted file mode 100755 (executable)
index 96101d0..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# Usage: cust_bill.cgi invnum
-#        http://server.name/path/cust_bill.cgi?invnum
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# this is a quick & ugly hack which does little more than add some formatting to the ascii output from /dbin/print-invoice
-#
-# ivan@voicenet.com 96-dec-05
-#
-# added navigation bar
-# ivan@voicenet.com 97-jan-30
-#
-# now uses Invoice.pm
-# ivan@voicenet.com 97-jun-30
-#
-# what to do if cust_bill search errors?
-# ivan@voicenet.com 97-jul-7
-#
-# s/FS::Search/FS::Record/; $cgisuidsetup($cgi); ivan@sisd.com 98-mar-14
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# also print 'printed' field ivan@sisd.com 98-jul-10
-
-use strict;
-use IO::File;
-use CGI::Base qw(:DEFAULT :CGI); # CGI module
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-use FS::Invoice;
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-
-#untaint invnum
-$QUERY_STRING =~ /^(\d+)$/;
-my($invnum)=$1;
-
-my($cust_bill) = qsearchs('cust_bill',{'invnum'=>$invnum});
-die "Invoice #$invnum not found!" unless $cust_bill;
-my($custnum) = $cust_bill->getfield('custnum');
-
-my($printed) = $cust_bill->printed;
-
-SendHeaders(); # one guess.
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Invoice View</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Invoice View</H1>
-    <A HREF="../view/cust_main.cgi?$custnum">View this customer (#$custnum)</A> | <A HREF="../">Main menu</A>
-    </CENTER><HR>
-    <BASEFONT SIZE=3>
-    <CENTER>
-      <A HREF="../edit/cust_pay.cgi?$invnum">Enter payments (check/cash) against this invoice</A>
-      <BR><A HREF="../misc/print-invoice.cgi?$invnum">Reprint this invoice</A>
-      <BR><BR>(Printed $printed times)
-    </CENTER>
-    <FONT SIZE=-1><PRE>
-END
-
-bless($cust_bill,"FS::Invoice");
-print $cust_bill->print_text;
-
-       #formatting
-       print <<END;
-    </PRE></FONT>
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/view/cust_main.cgi b/htdocs/view/cust_main.cgi
deleted file mode 100755 (executable)
index ca5fcd9..0000000
+++ /dev/null
@@ -1,336 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_main.cgi: View a customer
-#
-# Usage: cust_main.cgi custnum
-#        http://server.name/path/cust_main.cgi?custnum
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# the payment history section could use some work, see below
-# 
-# ivan@voicenet.com 96-nov-29 -> 96-dec-11
-#
-# added navigation bar (go to main menu ;)
-# ivan@voicenet.com 97-jan-30
-#
-# changes to the way credits/payments are applied (the links are here).
-# ivan@voicenet.com 97-apr-21
-#
-# added debugging code to diagnose CPU sucking problem.
-# ivan@voicenet.com 97-may-19
-#
-# CPU sucking problem was in comment code?  fixed?
-# ivan@voicenet.com 97-may-22
-#
-# rewrote for new API
-# ivan@voicenet.com 97-jul-22
-#
-# Changes to allow page to work at a relative position in server
-# Changed 'day' to 'daytime' because Pg6.3 reserves the day word
-#       bmccane@maxbaud.net     98-apr-3
-#
-# lose background, FS::CGI ivan@sisd.com 98-sep-2
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI); # CGI module
-use CGI::Carp qw(fatalsToBrowser);
-use Date::Format;
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs qsearch);
-use FS::CGI qw(header menubar);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-
-SendHeaders(); # one guess.
-print header("Customer View", menubar(
-  'Main Menu' => '../',
-)),<<END;
-    <BASEFONT SIZE=3>
-END
-
-#untaint custnum & get customer record
-$QUERY_STRING =~ /^(\d+)$/;
-my($custnum)=$1;
-my($cust_main)=qsearchs('cust_main',{'custnum'=>$custnum});
-die "Customer not found!" unless $cust_main;
-my($hashref)=$cust_main->hashref;
-
-#custnum
-print "<FONT SIZE=+1><CENTER>Customer #<B>$custnum</B></CENTER></FONT>",
-      qq!<CENTER><A HREF="#cust_main">Customer Information</A> | !,
-      qq!<A HREF="#cust_comments">Comments</A> | !,
-      qq!<A HREF="#cust_pkg">Packages</A> | !,
-      qq!<A HREF="#history">Payment History</A> </CENTER>!;
-
-#bill now linke
-print qq!<HR><CENTER><A HREF="../misc/bill.cgi?$custnum">!,
-      qq!Bill this customer now</A></CENTER>!;
-
-#formatting
-print qq!<HR><A NAME="cust_main"><CENTER><FONT SIZE=+1>Customer Information!,
-      qq!</FONT>!,
-      qq!<BR><A HREF="../edit/cust_main.cgi?$custnum!,
-      qq!">Edit this information</A></CENTER><FONT SIZE=-1>!;
-
-#agentnum
-my($agent)=qsearchs('agent',{
-  'agentnum' => $cust_main->getfield('agentnum')
-} );
-die "Agent not found!" unless $agent;
-print "<BR>Agent #<B>" , $agent->getfield('agentnum') , ": " ,
-                         $agent->getfield('agent') , "</B>";
-
-#refnum
-my($referral)=qsearchs('part_referral',{'refnum' => $cust_main->refnum});
-die "Referral not found!" unless $referral;
-print "<BR>Referral #<B>", $referral->refnum, ": ",
-      $referral->referral, "<\B>"; 
-
-#last, first
-print "<P><B>", $hashref->{'last'}, ", ", $hashref->{first}, "</B>";
-
-#ss
-print " (SS# <B>", $hashref->{ss}, "</B>)" if $hashref->{ss};
-
-#company
-print "<BR><B>", $hashref->{company}, "</B>" if $hashref->{company};
-
-#address1
-print "<BR><B>", $hashref->{address1}, "</B>";
-
-#address2
-print "<BR><B>", $hashref->{address2}, "</B>" if $hashref->{address2};
-
-#city
-print "<BR><B>", $hashref->{city}, "</B>";
-
-#county
-print " (<B>", $hashref->{county}, "</B> county)" if $hashref->{county};
-
-#state
-print ",<B>", $hashref->{state}, "</B>";
-
-#zip
-print "  <B>", $hashref->{zip}, "</B>";
-
-#country
-print "<BR><B>", $hashref->{country}, "</B>"
-  unless $hashref->{country} eq "US";
-
-#daytime
-print "<P><B>", $hashref->{daytime}, "</B>" if $hashref->{daytime};
-print " (Day)" if $hashref->{daytime} && $hashref->{night};
-
-#night
-print "<BR><B>", $hashref->{night}, "</B>" if $hashref->{night};
-print " (Night)" if $hashref->{daytime} && $hashref->{night};
-
-#fax
-print "<BR><B>", $hashref->{fax}, "</B> (Fax)" if $hashref->{fax};
-
-#payby/payinfo/paydate/payname
-if ($hashref->{payby} eq "CARD") {
-  print "<P>Card #<B>", $hashref->{payinfo}, "</B> Exp. <B>",
-    $hashref->{paydate}, "</B>";
-  print " (<B>", $hashref->{payname}, "</B>)" if $hashref->{payname};
-} elsif ($hashref->{payby} eq "BILL") {
-  print "<P>Bill";
-  print " on P.O. #<B>", $hashref->{payinfo}, "</B>"
-    if $hashref->{payinfo};
-  print " until <B>", $hashref->{paydate}, "</B>"
-    if $hashref->{paydate};
-  print " to <B>", $hashref->{payname}, "</B> at above address"
-    if $hashref->{payname};
-} elsif ($hashref->{payby} eq "COMP") {
-  print "<P>Access complimentary";
-  print " courtesy of <B>", $hashref->{payinfo}, "</B>"
-    if $hashref->{payinfo};
-  print " until <B>", $hashref->{paydate}, "</B>"
-    if $hashref->{paydate};
-} else {
-  print "Unknown payment type ", $hashref->{payby}, "!";
-}
-
-#tax
-print "<BR>(Tax exempt)" if $hashref->{tax};
-
-#otaker
-print "<P>Order taken by <B>", $hashref->{otaker}, "</B>";
-
-#formatting    
-print qq!<HR><FONT SIZE=+1><A NAME="cust_pkg"><CENTER>Packages</A></FONT>!,
-      qq!<BR>Click on package number to view/edit package.!,
-      qq!<BR><A HREF="../edit/cust_pkg.cgi?$custnum">Add/Edit packages</A>!,
-      qq!</CENTER><BR>!;
-
-#display packages
-
-#formatting
-print qq!<CENTER><TABLE BORDER=4>\n!,
-      qq!<TR><TH ROWSPAN=2>#</TH><TH ROWSPAN=2>Package</TH><TH COLSPAN=5>!,
-      qq!Dates</TH></TR>\n!,
-      qq!<TR><TH><FONT SIZE=-1>Setup</FONT></TH><TH>!,
-      qq!<FONT SIZE=-1>Next bill</FONT>!,
-      qq!</TH><TH><FONT SIZE=-1>Susp.</FONT></TH><TH><FONT SIZE=-1>Expire!,
-      qq!</FONT></TH>!,
-      qq!<TH><FONT SIZE=-1>Cancel</FONT></TH>!,
-      qq!</TR>\n!;
-
-#get package info
-my(@packages)=qsearch('cust_pkg',{'custnum'=>$custnum});
-my($package);
-foreach $package (@packages) {
-  my($pref)=$package->hashref;
-  my($part_pkg)=qsearchs('part_pkg',{
-    'pkgpart' => $pref->{pkgpart}
-  } );
-  print qq!<TR><TD><FONT SIZE=-1><A HREF="../view/cust_pkg.cgi?!,
-        $pref->{pkgnum}, qq!">!, 
-        $pref->{pkgnum}, qq!</A></FONT></TD>!,
-        "<TD><FONT SIZE=-1>", $part_pkg->getfield('pkg'), " - ",
-        $part_pkg->getfield('comment'), "</FONT></TD>",
-        "<TD><FONT SIZE=-1>", 
-        $pref->{setup} ? time2str("%D",$pref->{setup} ) : "" ,
-        "</FONT></TD>",
-        "<TD><FONT SIZE=-1>", 
-        $pref->{bill} ? time2str("%D",$pref->{bill} ) : "" ,
-        "</FONT></TD>",
-        "<TD><FONT SIZE=-1>",
-        $pref->{susp} ? time2str("%D",$pref->{susp} ) : "" ,
-        "</FONT></TD>",
-        "<TD><FONT SIZE=-1>",
-        $pref->{expire} ? time2str("%D",$pref->{expire} ) : "" ,
-        "</FONT></TD>",
-        "<TD><FONT SIZE=-1>",
-        $pref->{cancel} ? time2str("%D",$pref->{cancel} ) : "" ,
-        "</FONT></TD>",
-        "</TR>";
-}
-
-#formatting
-print "</TABLE></CENTER>";
-
-#formatting
-print qq!<CENTER><HR><A NAME="history"><FONT SIZE=+1>Payment History!,
-      qq!</FONT></A><BR>!,
-      qq!Click on invoice to view invoice/enter payment.<BR>!,
-      qq!<A HREF="../edit/cust_credit.cgi?$custnum">!,
-      qq!Post Credit / Refund</A></CENTER><BR>!;
-
-#get payment history
-#
-# major problem: this whole thing is way too sloppy.
-# minor problem: the description lines need better formatting.
-
-my(@history);
-
-my(@bills)=qsearch('cust_bill',{'custnum'=>$custnum});
-my($bill);
-foreach $bill (@bills) {
-  my($bref)=$bill->hashref;
-  push @history,
-    $bref->{_date} . qq!\t<A HREF="../view/cust_bill.cgi?! .
-    $bref->{invnum} . qq!">Invoice #! . $bref->{invnum} .
-    qq! (Balance \$! . $bref->{owed} . qq!)</A>\t! .
-    $bref->{charged} . qq!\t\t\t!;
-
-  my(@payments)=qsearch('cust_pay',{'invnum'=> $bref->{invnum} } );
-  my($payment);
-  foreach $payment (@payments) {
-#    my($pref)=$payment->hashref;
-    my($date,$invnum,$payby,$payinfo,$paid)=($payment->getfield('_date'),
-                                             $payment->getfield('invnum'),
-                                             $payment->getfield('payby'),
-                                             $payment->getfield('payinfo'),
-                                             $payment->getfield('paid'),
-                      );
-    push @history,
-      "$date\tPayment, Invoice #$invnum ($payby $payinfo)\t\t$paid\t\t";
-  }
-}
-
-my(@credits)=qsearch('cust_credit',{'custnum'=>$custnum});
-my($credit);
-foreach $credit (@credits) {
-  my($cref)=$credit->hashref;
-  push @history,
-    $cref->{_date} . "\tCredit #" . $cref->{crednum} . ", (Balance \$" .
-    $cref->{credited} . ") by " . $cref->{otaker} . " - " .
-    $cref->{reason} . "\t\t\t" . $cref->{amount} . "\t";
-
-  my(@refunds)=qsearch('cust_refund',{'crednum'=> $cref->{crednum} } );
-  my($refund);
-  foreach $refund (@refunds) {
-    my($rref)=$refund->hashref;
-    push @history,
-      $rref->{_date} . "\tRefund, Credit #" . $rref->{crednum} . " (" .
-      $rref->{payby} . " " . $rref->{payinfo} . ") by " .
-      $rref->{otaker} . " - ". $rref->{reason} . "\t\t\t\t" .
-      $rref->{refund};
-  }
-}
-
-        #formatting
-        print <<END;
-<CENTER><TABLE BORDER=4>
-<TR>
-  <TH>Date</TH>
-  <TH>Description</TH>
-  <TH><FONT SIZE=-1>Charge</FONT></TH>
-  <TH><FONT SIZE=-1>Payment</FONT></TH>
-  <TH><FONT SIZE=-1>In-house<BR>Credit</FONT></TH>
-  <TH><FONT SIZE=-1>Refund</FONT></TH>
-  <TH><FONT SIZE=-1>Balance</FONT></TH>
-</TR>
-END
-
-#display payment history
-
-my($balance)=0;
-my($item);
-foreach $item (sort keyfield_numerically @history) {
-  my($date,$desc,$charge,$payment,$credit,$refund)=split(/\t/,$item);
-  $charge ||= 0;
-  $payment ||= 0;
-  $credit ||= 0;
-  $refund ||= 0;
-  $balance += $charge - $payment;
-  $balance -= $credit - $refund;
-
-  print "<TR><TD><FONT SIZE=-1>",time2str("%D",$date),"</FONT></TD>",
-       "<TD><FONT SIZE=-1>$desc</FONT></TD>",
-       "<TD><FONT SIZE=-1>",
-        ( $charge ? "\$".sprintf("%.2f",$charge) : '' ),
-        "</FONT></TD>",
-       "<TD><FONT SIZE=-1>",
-        ( $payment ? "- \$".sprintf("%.2f",$payment) : '' ),
-        "</FONT></TD>",
-       "<TD><FONT SIZE=-1>",
-        ( $credit ? "- \$".sprintf("%.2f",$credit) : '' ),
-        "</FONT></TD>",
-       "<TD><FONT SIZE=-1>",
-        ( $refund ? "\$".sprintf("%.2f",$refund) : '' ),
-        "</FONT></TD>",
-       "<TD><FONT SIZE=-1>\$" . sprintf("%.2f",$balance),
-        "</FONT></TD>",
-        "\n";
-}
-
-#formatting
-print "</TABLE></CENTER>";
-
-#end
-
-#formatting
-print <<END;
-
-  </BODY>
-</HTML>
-END
-
-#subroutiens
-sub keyfield_numerically { (split(/\t/,$a))[0] <=> (split(/\t/,$b))[0] ; }
-
diff --git a/htdocs/view/cust_pkg.cgi b/htdocs/view/cust_pkg.cgi
deleted file mode 100755 (executable)
index 04e3832..0000000
+++ /dev/null
@@ -1,181 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# cust_pkg.cgi: View a package
-#
-# Usage: cust_pkg.cgi pkgnum
-#        http://server.name/path/cust_pkg.cgi?pkgnum
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# ivan@voicenet.com 96-dec-15
-#
-# services section needs to be cleaned up, needs to display extraneous
-# entries in cust_pkg!
-# ivan@voicenet.com 96-dec-31
-#
-# added navigation bar
-# ivan@voicenet.com 97-jan-30
-#
-# changed and fixed up suspension and cancel stuff, now you can't add
-# services to a cancelled package
-# ivan@voicenet.com 97-feb-27
-#
-# rewrote for new API, still needs to be cleaned up!
-# ivan@voicenet.com 97-jul-29
-#
-# no FS::Search ivan@sisd.com 98-mar-7
-
-use strict;
-use Date::Format;
-use CGI::Base qw(:DEFAULT :CGI); # CGI module
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-
-my(%uiview,%uiadd);
-my($part_svc);
-foreach $part_svc ( qsearch('part_svc',{}) ) {
-  $uiview{$part_svc->svcpart}="../view/". $part_svc->svcdb . ".cgi";
-  $uiadd{$part_svc->svcpart}="../edit/". $part_svc->svcdb . ".cgi";
-}
-
-SendHeaders(); # one guess.
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Package View</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H1>Package View</H1>
-    </CENTER>
-    <BASEFONT SIZE=3>
-END
-
-#untaint pkgnum
-$QUERY_STRING =~ /^(\d+)$/;
-my($pkgnum)=$1;
-
-#get package record
-my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-die "No package!" unless $cust_pkg;
-my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->getfield('pkgpart')});
-
-#nav bar
-my($custnum)=$cust_pkg->getfield('custnum');
-print qq!<CENTER><A HREF="../view/cust_main.cgi?$custnum">View this customer!,
-      qq! (#$custnum)</A> | <A HREF="../">Main menu</A></CENTER><BR>!;
-
-#print info
-my($susp,$cancel,$expire)=(
-  $cust_pkg->getfield('susp'),
-  $cust_pkg->getfield('cancel'),
-  $cust_pkg->getfield('expire'),
-);
-print "<FONT SIZE=+1><CENTER>Package #<B>$pkgnum</B></FONT>";
-print qq!<BR><A HREF="#package">Package Information</A>!;
-print qq! | <A HREF="#services">Service Information</A>! unless $cancel;
-print qq!</CENTER><HR>\n!;
-
-my($pkg,$comment)=($part_pkg->getfield('pkg'),$part_pkg->getfield('comment'));
-print qq!<A NAME="package"><CENTER><FONT SIZE=+1>Package Information!,
-      qq!</FONT></A>!;
-print qq!<BR><A HREF="../unimp.html">Edit this information</A></CENTER>!;
-print "<P>Package: <B>$pkg - $comment</B>";
-
-my($setup,$bill)=($cust_pkg->getfield('setup'),$cust_pkg->getfield('bill'));
-print "<BR>Setup: <B>", $setup ? time2str("%D",$setup) : "(Not setup)" ,"</B>";
-print "<BR>Next bill: <B>", $bill ? time2str("%D",$bill) : "" ,"</B>";
-
-if ($susp) {
-  print "<BR>Suspended: <B>", time2str("%D",$susp), "</B>";
-  print qq! <A HREF="../misc/unsusp_pkg.cgi?$pkgnum">Unsuspend</A>! unless $cancel;
-} else {
-  print qq!<BR><A HREF="../misc/susp_pkg.cgi?$pkgnum">Suspend</A>! unless $cancel;
-}
-
-if ($expire) {
-  print "<BR>Expire: <B>", time2str("%D",$expire), "</B>";
-}
-  print <<END;
-<FORM ACTION="../misc/expire_pkg.cgi" METHOD="post">
-<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
-Expire (date): <INPUT TYPE="text" NAME="date" VALUE="" >
-<INPUT TYPE="submit" VALUE="Cancel later">
-END
-
-if ($cancel) {
-  print "<BR>Cancelled: <B>", time2str("%D",$cancel), "</B>";
-} else {
-  print qq!<BR><A HREF="../misc/cancel_pkg.cgi?$pkgnum">Cancel now</A>!;
-}
-
-#otaker
-my($otaker)=$cust_pkg->getfield('otaker');
-print "<P>Order taken by <B>$otaker</B>";
-
-unless ($cancel) {
-
-  #services
-  print <<END;
-<HR><A NAME="services"><CENTER><FONT SIZE=+1>Service Information</FONT></A>
-<BR>Click on service to view/edit/add service.</CENTER><BR>
-<CENTER><B>Do NOT pick the "Link to existing" option unless you are auditing!!!</B></CENTER>
-<CENTER><TABLE BORDER=4>
-<TR><TH>Service</TH>
-END
-
-  #list of services this pkgpart includes
-  my($pkg_svc,%pkg_svc);
-  foreach $pkg_svc ( qsearch('pkg_svc',{'pkgpart'=> $cust_pkg->pkgpart }) ) {
-    $pkg_svc{$pkg_svc->svcpart} = $pkg_svc->quantity if $pkg_svc->quantity;
-  }
-
-  #list of records from cust_svc
-  my($svcpart);
-  foreach $svcpart (sort {$a <=> $b} keys %pkg_svc) {
-
-    my($svc)=qsearchs('part_svc',{'svcpart'=>$svcpart})->getfield('svc');
-
-    my(@cust_svc)=qsearch('cust_svc',{'pkgnum'=>$pkgnum, 
-                                      'svcpart'=>$svcpart,
-                                     });
-
-    my($enum);
-    for $enum ( 1 .. $pkg_svc{$svcpart} ) {
-
-      my($cust_svc);
-      if ( $cust_svc=shift @cust_svc ) {
-        my($svcnum)=$cust_svc->svcnum;
-        print <<END;
-<TR><TD><A HREF="$uiview{$svcpart}?$svcnum">(View) $svc<A></TD></TR>
-END
-      } else {
-        print <<END;
-<TR>
-  <TD><A HREF="$uiadd{$svcpart}?pkgnum$pkgnum-svcpart$svcpart">
-      (Add) $svc</A>
-   or <A HREF="../misc/link.cgi?pkgnum$pkgnum-svcpart$svcpart">
-      (Link to existing) $svc</A>
-  </TD>
-</TR>
-END
-      }
-
-    }
-    warn "WARNING: Leftover services pkgnum $pkgnum!" if @cust_svc;; 
-  }
-
-  print "</TABLE></CENTER>";
-
-}
-
-#formatting
-print <<END;
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/view/svc_acct.cgi b/htdocs/view/svc_acct.cgi
deleted file mode 100755 (executable)
index 7096c2f..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# View svc_acct records
-#
-# Usage: svc_acct.cgi svcnum
-#        http://server.name/path/svc_acct.cgi?svcnum
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# ivan@voicenet.com 96-dec-17
-#
-# added link to send info
-# ivan@voicenet.com 97-jan-4
-#
-# added navigation bar and ability to change username, etc.
-# ivan@voicenet.com 97-jan-30
-#
-# activate 800 service
-# ivan@voicenet.com 97-feb-10
-#
-# modified navbar code (should be a subroutine?), added link to cancel account (only if not audited)
-# ivan@voicenet.com 97-apr-16
-#
-# INCOMPLETELY rewrote some things for new API
-# ivan@voicenet.com 97-jul-29
-#
-# FS::Search became FS::Record, use strict, etc. ivan@sisd.com 98-mar-9
-#
-# Changes to allow page to work at a relative position in server
-# Changed 'password' to '_password' because Pg6.3 reserves the password word
-#       bmccane@maxbaud.net     98-apr-3
-#
-# /var/spool/freeside/conf/domain ivan@sisd.com 98-jul-17
-#
-# displays arbitrary radius attributes ivan@sisd.com 98-aug-16
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI);
-use CGI::Carp qw(fatalsToBrowser);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs fields);
-
-my($conf_domain)="/var/spool/freeside/conf/domain";
-open(DOMAIN,$conf_domain) or die "Can't open $conf_domain: $!";
-my($mydomain)=map {
-  /^(.*)$/ or die "Illegal line in $conf_domain!"; #yes, we trust the file
-  $1;
-} grep $_ !~ /^(#|$)/, <DOMAIN>;
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-&cgisuidsetup($cgi);
-
-#untaint svcnum
-$QUERY_STRING =~ /^(\d+)$/;
-my($svcnum)=$1;
-my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$svcnum});
-die "Unkonwn svcnum" unless $svc_acct;
-
-my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum});
-my($pkgnum)=$cust_svc->getfield('pkgnum');
-my($cust_pkg,$custnum);
-if ($pkgnum) {
-  $cust_pkg=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-  $custnum=$cust_pkg->getfield('custnum');
-}
-
-my($part_svc)=qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } );
-die "Unkonwn svcpart" unless $part_svc;
-
-SendHeaders(); # one guess.
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Account View</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER><H1>Account View</H1>
-    <BASEFONT SIZE=3>
-<CENTER>
-END
-
-if ($pkgnum || $custnum) {
-  print <<END;
-<A HREF="../view/cust_pkg.cgi?$pkgnum">View this package (#$pkgnum)</A> | 
-<A HREF="../view/cust_main.cgi?$custnum">View this customer (#$custnum)</A> | 
-END
-} else {
-  print <<END;
-<A HREF="../misc/cancel-unaudited.cgi?$svcnum">Cancel this (unaudited)account</A> |
-END
-}
-
-print <<END;
-<A HREF="../">Main menu</A></CENTER><BR>
-<FONT SIZE=+1>Service #$svcnum</FONT>
-END
-
-print qq!<BR><A HREF="../edit/svc_acct.cgi?$svcnum">Edit this information</A>!;
-#print qq!<BR><A HREF="../misc/sendconfig.cgi?$svcnum">Send account information</A>!;
-print qq!<BR><BR><A HREF="#general">General</A> | <A HREF="#shell">Shell account</A> | !;
-print qq!<A HREF="#slip">SLIP/PPP account</A></CENTER>!;
-
-#formatting
-print qq!<HR><CENTER><FONT SIZE=+1><A NAME="general">General</A></FONT></CENTER>!;
-
-#svc
-print "Service: <B>", $part_svc->svc, "</B>";
-
-#username
-print "<BR>Username: <B>", $svc_acct->username, "</B>";
-
-#password
-if (substr($svc_acct->_password,0,1) eq "*") {
-  print "<BR>Password: <I>(Login disabled)</I><BR>";
-} else {
-  print "<BR>Password: <I>(hidden)</I><BR>";
-}
-
-# popnum -> svc_acct_pop record
-my($svc_acct_pop)=qsearchs('svc_acct_pop',{'popnum'=>$svc_acct->popnum});
-
-#pop
-print "POP: <B>", $svc_acct_pop->city, ", ", $svc_acct_pop->state,
-      " (", $svc_acct_pop->ac, ")/", $svc_acct_pop->exch, "<\B>"
-  if $svc_acct_pop;
-
-#shell account
-print qq!<HR><CENTER><FONT SIZE=+1><A NAME="shell">!;
-if ($svc_acct->uid ne '') {
-  print "Shell account";
-  print "</A></FONT></CENTER>";
-  print "Uid: <B>", $svc_acct->uid, "</B>";
-  print "<BR>Gid: <B>", $svc_acct->gid, "</B>";
-
-  print qq!<BR>Finger name: <B>!, $svc_acct->finger, qq!</B><BR>!;
-
-  print "Home directory: <B>", $svc_acct->dir, "</B><BR>";
-
-  print "Shell: <B>", $svc_acct->shell, "</B><BR>";
-
-  print "Quota: <B>", $svc_acct->quota, "</B> <I>(unimplemented)</I>";
-} else {
-  print "No shell account.</A></FONT></CENTER>";
-}
-
-# SLIP/PPP
-print qq!<HR><CENTER><FONT SIZE=+1><A NAME="slip">!;
-if ($svc_acct->slipip) {
-  print "SLIP/PPP account</A></FONT></CENTER>";
-  print "IP address: <B>", ( $svc_acct->slipip eq "0.0.0.0" || $svc_acct->slipip eq '0e0' ) ? "<I>(Dynamic)</I>" : $svc_acct->slipip ,"</B>";
-  my($attribute);
-  foreach $attribute ( grep /^radius_/, fields('svc_acct') ) {
-    #warn $attribute;
-    $attribute =~ /^radius_(.*)$/;
-    my($pattribute) = ($1);
-    $pattribute =~ s/_/-/g;
-    print "<BR>Radius $pattribute: <B>". $svc_acct->getfield($attribute), "</B>";
-  }
-} else {
-  print "No SLIP/PPP account</A></FONT></CENTER>"
-}
-
-print "<HR>";
-
-       #formatting
-       print <<END;
-
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/view/svc_acct_sm.cgi b/htdocs/view/svc_acct_sm.cgi
deleted file mode 100755 (executable)
index 42623ee..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# View svc_acct_sm records
-#
-# Usage: svc_acct_sm.cgi svcnum
-#        http://server.name/path/svc_acct_sm.cgi?svcnum
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# based on view/svc_acct.cgi
-# 
-# ivan@voicenet.com 97-jan-5
-#
-# added navigation bar
-# ivan@voicenet.com 97-jan-30
-# 
-# rewrite ivan@sisd.com 98-mar-15
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-#
-# /var/spool/freeside/conf/domain ivan@sisd.com 98-jul-17
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-
-my($conf_domain)="/var/spool/freeside/conf/domain";
-open(DOMAIN,$conf_domain) or die "Can't open $conf_domain: $!";
-my($mydomain)=map {
-  /^(.*)$/ or die "Illegal line in $conf_domain!"; #yes, we trust the file
-  $1
-} grep $_ !~ /^(#|$)/, <DOMAIN>;
-close DOMAIN;
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-cgisuidsetup($cgi);
-
-#untaint svcnum
-$QUERY_STRING =~ /^(\d+)$/;
-my($svcnum)=$1;
-my($svc_acct_sm)=qsearchs('svc_acct_sm',{'svcnum'=>$svcnum});
-die "Unknown svcnum" unless $svc_acct_sm;
-
-my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum});
-my($pkgnum)=$cust_svc->getfield('pkgnum');
-my($cust_pkg,$custnum);
-if ($pkgnum) {
-  $cust_pkg=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-  $custnum=$cust_pkg->getfield('custnum');
-}
-
-my($part_svc)=qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } );
-die "Unkonwn svcpart" unless $part_svc;
-
-SendHeaders(); # one guess.
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Mail Alias View</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER><H1>Mail Alias View</H1>
-END
-if ($pkgnum || $custnum) {
-  print <<END;
-<A HREF="../view/cust_pkg.cgi?$pkgnum">View this package (#$pkgnum)</A> | 
-<A HREF="../view/cust_main.cgi?$custnum">View this customer (#$custnum)</A> | 
-END
-} else {
-  print <<END;
-<A HREF="../misc/cancel-unaudited.cgi?$svcnum">Cancel this (unaudited)account</A> |
-END
-}
-
-print <<END;
-    <A HREF="../">Main menu</A></CENTER><BR<
-    <FONT SIZE=+1>Service #$svcnum</FONT>
-    <P><A HREF="../edit/svc_acct_sm.cgi?$svcnum">Edit this information</A>
-    <BASEFONT SIZE=3>
-END
-
-my($domsvc,$domuid,$domuser)=(
-  $svc_acct_sm->domsvc,
-  $svc_acct_sm->domuid,
-  $svc_acct_sm->domuser,
-);
-my($svc) = $part_svc->svc;
-my($svc_domain)=qsearchs('svc_domain',{'svcnum'=>$domsvc});
-my($domain)=$svc_domain->domain;
-my($svc_acct)=qsearchs('svc_acct',{'uid'=>$domuid});
-my($username)=$svc_acct->username;
-
-#formatting
-print qq!<HR>!;
-
-#svc
-print "Service: <B>$svc</B>";
-
-print "<HR>";
-
-print qq!Mail to <B>!, ( ($domuser eq '*') ? "<I>(anything)</I>" : $domuser ) , qq!</B>\@<B>$domain</B> forwards to <B>$username</B>\@$mydomain mailbox.!;
-
-print "<HR>";
-
-       #formatting
-       print <<END;
-
-  </BODY>
-</HTML>
-END
-
diff --git a/htdocs/view/svc_domain.cgi b/htdocs/view/svc_domain.cgi
deleted file mode 100755 (executable)
index 78ff6ac..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/perl -Tw
-#
-# View svc_domain records
-#
-# Usage: svc_domain svcnum
-#        http://server.name/path/svc_domain.cgi?svcnum
-#
-# Note: Should be run setuid freeside as user nobody.
-#
-# ivan@voicenet.com 97-jan-6
-#
-# rewrite ivan@sisd.com 98-mar-14
-#
-# Changes to allow page to work at a relative position in server
-#       bmccane@maxbaud.net     98-apr-3
-
-use strict;
-use CGI::Base qw(:DEFAULT :CGI);
-use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearchs);
-
-my($cgi) = new CGI::Base;
-$cgi->get;
-cgisuidsetup($cgi);
-
-#untaint svcnum
-$QUERY_STRING =~ /^(\d+)$/;
-my($svcnum)=$1;
-my($svc_domain)=qsearchs('svc_domain',{'svcnum'=>$svcnum});
-die "Unknown svcnum" unless $svc_domain;
-my($domain)=$svc_domain->domain;
-
-my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum});
-my($pkgnum)=$cust_svc->getfield('pkgnum');
-my($cust_pkg,$custnum);
-if ($pkgnum) {
-  $cust_pkg=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-  $custnum=$cust_pkg->getfield('custnum');
-}
-
-my($part_svc)=qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } );
-die "Unkonwn svcpart" unless $part_svc;
-
-SendHeaders(); # one guess.
-print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Domain View</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER><H1>Domain View</H1>
-    <BASEFONT SIZE=3>
-<CENTER>
-<A HREF="../view/cust_pkg.cgi?$pkgnum">View this package (#$pkgnum)</A> | 
-<A HREF="../view/cust_main.cgi?$custnum">View this customer (#$custnum)</A> | 
-<A HREF="../">Main menu</A></CENTER><BR>
-    <FONT SIZE=+1>Service #$svcnum</FONT>
-    </CENTER>
-END
-
-print "<HR>";
-print "Service: <B>", $part_svc->svc, "</B>";
-print "<HR>";
-
-print qq!Domain name <B>$domain</B>.!;
-print qq!<P><A HREF="http://rs.internic.net/cgi-bin/whois?do+$domain">View whois information.</A>!;
-
-print "<HR>";
-
-       #formatting
-       print <<END;
-
-  </BODY>
-</HTML>
-END
-
diff --git a/htetc/global.asa b/htetc/global.asa
new file mode 100644 (file)
index 0000000..d87f1ea
--- /dev/null
@@ -0,0 +1,193 @@
+BEGIN { eval "use Devel::AutoProfiler;"; } #only if installed...
+#BEGIN { package Devel::AutoProfiler; use vars qw(%caller_info); }
+#use Devel::AutoProfiler;
+
+use strict;
+use vars qw( $cgi $p );
+use CGI;
+#use CGI::Carp qw(fatalsToBrowser);
+use Date::Format;
+use Date::Parse;
+use Time::Local;
+use Tie::IxHash;
+use HTML::Entities;
+use IO::Handle;
+use IO::File;
+use String::Approx qw(amatch);
+use Chart::LinesPoints;
+use HTML::Widgets::SelectLayers 0.02;
+use FS::UID qw(cgisuidsetup dbh getotaker datasrc driver_name);
+use FS::Record qw(qsearch qsearchs fields dbdef);
+use FS::Conf;
+use FS::CGI qw(header menubar popurl table itable ntable idiot eidiot
+               small_custview myexit http_header);
+use FS::Msgcat qw(gettext geterror);
+
+use FS::agent;
+use FS::agent_type;
+use FS::domain_record;
+use FS::cust_bill;
+use FS::cust_bill_pay;
+use FS::cust_credit;
+use FS::cust_credit_bill;
+use FS::cust_main;
+use FS::cust_main_county;
+use FS::cust_pay;
+use FS::cust_pkg;
+use FS::cust_refund;
+use FS::cust_svc;
+use FS::nas;
+use FS::part_bill_event;
+use FS::part_pkg;
+use FS::part_referral;
+use FS::part_svc;
+use FS::part_svc_router;
+use FS::pkg_svc;
+use FS::port;
+use FS::queue qw(joblisting);
+use FS::raddb;
+use FS::session;
+use FS::svc_acct;
+use FS::svc_acct_pop qw(popselector);
+use FS::svc_domain;
+use FS::svc_forward;
+use FS::svc_www;
+use FS::router;
+use FS::part_router_field;
+use FS::router_field;
+use FS::addr_block;
+use FS::part_sb_field;
+use FS::sb_field;
+use FS::svc_broadband;
+use FS::type_pkgs;
+use FS::part_export;
+use FS::part_export_option;
+use FS::export_svc;
+use FS::msgcat;
+
+sub Script_OnStart {
+  $Response->AddHeader('Pragma' => 'no-cache');
+  $Response->AddHeader('Cache-control' => 'no-cache');
+#  $Response->AddHeader('Expires' => 0);
+  $Response->{Expires} = -36288000;
+
+  $cgi = new CGI;
+  &cgisuidsetup($cgi);
+  $p = popurl(2);
+  #print $cgi->header( '-expires' => 'now' );
+  dbh->{'private_profile'} = {} if dbh->can('sprintProfile');
+
+  #really should check for FS::Profiler or something
+    # Devel::AutoProfiler _our_ VERSION?  thanks a fucking lot
+  if ( Devel::AutoProfiler->can('__recursively_fetch_subs_in_package') ) {
+    #should check to see it's my special version.  well, switch to FS::Profiler
+
+    #nicked from Devel::AutoProfiler::INIT
+    my %subs = Devel::AutoProfiler::__recursively_fetch_subs_in_package('main');
+
+
+    SUB : while( my ($name, $ref) = each(%subs) )
+      {
+        #next if $name =~ /^(main::)?Apache::/;
+        next unless $name =~ /FS/;
+        foreach my $sub (@Devel::AutoProfiler::do_not_instrument_this_sub)
+          {
+            if ($name =~ /$sub/)
+              {
+                next SUB;
+              }
+          }
+        next if ($Devel::AutoProfiler::do_not_instrument_this_sub{$name});
+        #warn "INIT name is $name \n";
+        Devel::AutoProfiler::__instrument_sub($name, $ref);
+      }
+
+  }
+
+}
+
+sub Script_OnFlush {
+  my $ref = $Response->{BinaryRef};
+  #$$ref = $cgi->header( @FS::CGI::header ) . $$ref;
+  #$$ref = $cgi->header() . $$ref;
+  #warn "Script_OnFlush called with dbh ". dbh. "\n";
+  #if ( dbh->can('sprintProfile') ) {
+  if ( UNIVERSAL::can(dbh,'sprintProfile') ) {
+    #warn "dbh can sprintProfile\n";
+    if ( lc($Response->{ContentType}) eq 'text/html' ) { #con
+      #warn "contenttype is sprintProfile\n";
+      $$ref =~ s/<\/BODY>[\s\n]*<\/HTML>[\s\n]*$//i
+        or warn "can't remove";
+  
+      #$$ref .= '<PRE>'. ("\n"x96). encode_entities(dbh->sprintProfile()). '</PRE>';
+      #  wtf?  konqueror...
+      $$ref .= '<PRE>'. ("\n"x4096). encode_entities(dbh->sprintProfile()).
+               "\n\n". &sprintAutoProfile(). '</PRE>';
+
+      $$ref .= '</BODY></HTML>';
+    }
+    dbh->{'private_profile'} = {};
+  }
+}
+
+#if ( defined(@DBIx::Profile::ISA) && DBIx::Profile::db->can('sprintProfile') ) {
+#if ( defined(@DBIx::Profile::ISA) && UNIVERSAL::can('DBIx::Profile::db', 'sprintProfile') ) {
+if ( defined(@DBIx::Profile::ISA) ) {
+
+  #warn "enabling profiling redirects";
+  *CGI::redirect = sub {
+    my( $self, $location) = @_;
+    my $page =
+      $cgi->header.
+      qq!<HTML><BODY>Redirect to <A HREF="$location">$location</A><BR><BR>!.
+      '<PRE>'. encode_entities(dbh->sprintProfile()).
+      "\n\n". &sprintAutoProfile().  '</PRE>'.
+      '</BODY></HTML>';
+    dbh->{'private_profile'} = {};
+    return $page;
+  };
+
+}
+
+sub by_total_time 
+{ 
+  return $a->{total_time_in_sub} <=> $b->{total_time_in_sub}; 
+}
+
+sub sprintAutoProfile {
+  my %caller_info = %Devel::AutoProfiler::caller_info;
+  return unless keys %caller_info;
+
+  %Devel::AutoProfiler::caller_info = ();
+
+  my @keys = keys(%caller_info);
+
+  foreach my $key (@keys)
+    {
+      my $href = $caller_info{$key};
+
+      $href->{who_am_i} = $key;
+    }
+
+  my @subs = values(%caller_info);
+
+  #my @sorted = sort by_total_time ( @subs );
+  my @sorted = reverse sort by_total_time ( @subs );
+
+  # print Dumper \@sorted;
+
+  my @readable_info;
+
+  foreach my $sort (@sorted)
+    {
+      push(@readable_info, delete($sort->{who_am_i}));
+      push(@readable_info, $sort);
+    }
+
+  use Data::Dumper;
+  return encode_entities(Dumper(\@readable_info));
+
+}
+
+1;
+
diff --git a/htetc/handler.pl b/htetc/handler.pl
new file mode 100644 (file)
index 0000000..481d5a2
--- /dev/null
@@ -0,0 +1,166 @@
+#!/usr/bin/perl
+#
+# This is a basic, fairly fuctional Mason handler.pl.
+#
+# For something a little more involved, check out session_handler.pl
+
+package HTML::Mason;
+
+# Bring in main Mason package.
+use HTML::Mason;
+
+# Bring in ApacheHandler, necessary for mod_perl integration.
+# Uncomment the second line (and comment the first) to use
+# Apache::Request instead of CGI.pm to parse arguments.
+use HTML::Mason::ApacheHandler;
+# use HTML::Mason::ApacheHandler (args_method=>'mod_perl');
+
+# Uncomment the next line if you plan to use the Mason previewer.
+#use HTML::Mason::Preview;
+
+use strict;
+
+# List of modules that you want to use from components (see Admin
+# manual for details)
+#{  package HTML::Mason::Commands;
+#   use CGI;
+#}
+
+# Create Mason objects
+#
+
+#my $parser = new HTML::Mason::Parser;
+#my $interp = new HTML::Mason::Interp (parser=>$parser,
+#                                      comp_root=>'/var/www/masondocs',
+#                                      data_dir=>'/usr/local/etc/freeside/masondata',
+#                                      out_mode=>'stream',
+#                                     );
+my $ah = new HTML::Mason::ApacheHandler (
+  #interp => $interp,
+  #auto_send_headers => 0,
+  comp_root=>'/var/www/freeside',
+  data_dir=>'/usr/local/etc/freeside/masondata',
+  #out_mode=>'stream',
+);
+
+# Activate the following if running httpd as root (the normal case).
+# Resets ownership of all files created by Mason at startup.
+#
+#chown (Apache->server->uid, Apache->server->gid, $interp->files_written);
+
+sub handler
+{
+    my ($r) = @_;
+
+    # If you plan to intermix images in the same directory as
+    # components, activate the following to prevent Mason from
+    # evaluating image files as components.
+    #
+    #return -1 if $r->content_type && $r->content_type !~ m|^text/|i;
+
+    #rar
+    { package HTML::Mason::Commands;
+      use strict;
+      use vars qw( $cgi $p );
+      use CGI;
+      #use CGI::Carp qw(fatalsToBrowser);
+      use Date::Format;
+      use Date::Parse;
+      use Time::Local;
+      use Tie::IxHash;
+      use HTML::Entities;
+      use IO::Handle;
+      use IO::File;
+      use String::Approx qw(amatch);
+      use Chart::LinesPoints;
+      use HTML::Widgets::SelectLayers 0.02;
+      use FS::UID qw(cgisuidsetup dbh getotaker datasrc driver_name);
+      use FS::Record qw(qsearch qsearchs fields dbdef);
+      use FS::Conf;
+      use FS::CGI qw(header menubar popurl table itable ntable idiot eidiot
+                     small_custview myexit http_header);
+      use FS::Msgcat qw(gettext geterror);
+
+      use FS::agent;
+      use FS::agent_type;
+      use FS::domain_record;
+      use FS::cust_bill;
+      use FS::cust_bill_pay;
+      use FS::cust_credit;
+      use FS::cust_credit_bill;
+      use FS::cust_main;
+      use FS::cust_main_county;
+      use FS::cust_pay;
+      use FS::cust_pkg;
+      use FS::cust_refund;
+      use FS::cust_svc;
+      use FS::nas;
+      use FS::part_bill_event;
+      use FS::part_pkg;
+      use FS::part_referral;
+      use FS::part_svc;
+      use FS::part_svc_router;
+      use FS::pkg_svc;
+      use FS::port;
+      use FS::queue qw(joblisting);
+      use FS::raddb;
+      use FS::session;
+      use FS::svc_acct;
+      use FS::svc_acct_pop qw(popselector);
+      use FS::svc_acct_sm;
+      use FS::svc_domain;
+      use FS::svc_forward;
+      use FS::svc_www;
+      use FS::router;
+      use FS::part_router_field;
+      use FS::router_field;
+      use FS::addr_block;
+      use FS::part_sb_field;
+      use FS::sb_field;
+      use FS::svc_broadband;
+      use FS::type_pkgs;
+      use FS::part_export;
+      use FS::part_export_option;
+      use FS::export_svc;
+      use FS::msgcat;
+
+      *CGI::redirect = sub {
+        my( $self, $location ) = @_;
+        use vars qw($m);
+        #http://www.masonhq.com/docs/faq/#how_do_i_do_an_external_redirect
+        $m->clear_buffer;
+        # The next two lines are necessary to stop Apache from re-reading
+        # POSTed data.
+        $r->method('GET');
+        $r->headers_in->unset('Content-length');
+        $r->content_type('text/html');
+        #$r->err_header_out('Location' => $location);
+        $r->header_out('Location' => $location);
+         $r->header_out('Content-Type' => 'text/html');
+         $m->abort(302);
+
+        '';
+      };
+
+      $cgi = new CGI;
+      &cgisuidsetup($cgi);
+      #&cgisuidsetup($r);
+      $p = popurl(2);
+    }
+
+    $r->content_type('text/html');
+    #eorar
+
+    my $headers = $r->headers_out;
+    $headers->{'Pragma'} = $headers->{'Cache-control'} = 'no-cache';
+    #$r->no_cache(1);
+    $headers->{'Expires'} = '0';
+
+#    $r->send_http_header;
+
+    my $status = $ah->handle_request($r);
+
+    $status;
+}
+
+1;
diff --git a/htetc/handler.pl-1.0x b/htetc/handler.pl-1.0x
new file mode 100644 (file)
index 0000000..8840b08
--- /dev/null
@@ -0,0 +1,161 @@
+#!/usr/bin/perl
+#
+# This is a basic, fairly fuctional Mason handler.pl.
+#
+# For something a little more involved, check out session_handler.pl
+
+package HTML::Mason;
+
+# Bring in main Mason package.
+use HTML::Mason;
+
+# Bring in ApacheHandler, necessary for mod_perl integration.
+# Uncomment the second line (and comment the first) to use
+# Apache::Request instead of CGI.pm to parse arguments.
+use HTML::Mason::ApacheHandler;
+# use HTML::Mason::ApacheHandler (args_method=>'mod_perl');
+
+# Uncomment the next line if you plan to use the Mason previewer.
+#use HTML::Mason::Preview;
+
+use strict;
+
+# List of modules that you want to use from components (see Admin
+# manual for details)
+#{  package HTML::Mason::Commands;
+#   use CGI;
+#}
+
+# Create Mason objects
+#
+my $parser = new HTML::Mason::Parser;
+my $interp = new HTML::Mason::Interp (parser=>$parser,
+                                      comp_root=>'/var/www/freeside',
+                                      data_dir=>'/usr/local/etc/freeside/masondata',
+                                      out_mode=>'stream',
+                                     );
+my $ah = new HTML::Mason::ApacheHandler ( interp => $interp,
+                                          #auto_send_headers => 0,
+                                        );
+
+# Activate the following if running httpd as root (the normal case).
+# Resets ownership of all files created by Mason at startup.
+#
+chown (Apache->server->uid, Apache->server->gid, $interp->files_written);
+
+sub handler
+{
+    my ($r) = @_;
+
+    # If you plan to intermix images in the same directory as
+    # components, activate the following to prevent Mason from
+    # evaluating image files as components.
+    #
+    #return -1 if $r->content_type && $r->content_type !~ m|^text/|i;
+
+    #rar
+    { package HTML::Mason::Commands;
+      use strict;
+      use vars qw( $cgi $p );
+      use CGI;
+      #use CGI::Carp qw(fatalsToBrowser);
+      use Date::Format;
+      use Date::Parse;
+      use Time::Local;
+      use Tie::IxHash;
+      use HTML::Entities;
+      use IO::Handle;
+      use IO::File;
+      use String::Approx qw(amatch);
+      use Chart::LinesPoints;
+      use HTML::Widgets::SelectLayers 0.02;
+      use FS::UID qw(cgisuidsetup dbh getotaker datasrc driver_name);
+      use FS::Record qw(qsearch qsearchs fields dbdef);
+      use FS::Conf;
+      use FS::CGI qw(header menubar popurl table itable ntable idiot eidiot
+                     small_custview myexit http_header);
+      use FS::Msgcat qw(gettext geterror);
+
+      use FS::agent;
+      use FS::agent_type;
+      use FS::domain_record;
+      use FS::cust_bill;
+      use FS::cust_bill_pay;
+      use FS::cust_credit;
+      use FS::cust_credit_bill;
+      use FS::cust_main;
+      use FS::cust_main_county;
+      use FS::cust_pay;
+      use FS::cust_pkg;
+      use FS::cust_refund;
+      use FS::cust_svc;
+      use FS::nas;
+      use FS::part_bill_event;
+      use FS::part_pkg;
+      use FS::part_referral;
+      use FS::part_svc;
+      use FS::part_svc_router;
+      use FS::pkg_svc;
+      use FS::port;
+      use FS::queue qw(joblisting);
+      use FS::raddb;
+      use FS::session;
+      use FS::svc_acct;
+      use FS::svc_acct_pop qw(popselector);
+      use FS::svc_acct_sm;
+      use FS::svc_domain;
+      use FS::svc_forward;
+      use FS::svc_www;
+      use FS::router;
+      use FS::part_router_field;
+      use FS::router_field;
+      use FS::addr_block;
+      use FS::part_sb_field;
+      use FS::sb_field;
+      use FS::svc_broadband;
+      use FS::type_pkgs;
+      use FS::part_export;
+      use FS::part_export_option;
+      use FS::export_svc;
+      use FS::msgcat;
+
+      *CGI::redirect = sub {
+        my( $self, $location ) = @_;
+
+        #http://www.masonhq.com/docs/faq/#how_do_i_do_an_external_redirect
+        $m->clear_buffer;
+        # The next two lines are necessary to stop Apache from re-reading
+        # POSTed data.
+        $r->method('GET');
+        $r->headers_in->unset('Content-length');
+        $r->content_type('text/html');
+        #$r->err_header_out('Location' => $location);
+        $r->header_out('Location' => $location);
+         $r->header_out('Content-Type' => 'text/html');
+         $m->abort(302);
+
+        '';
+      };
+
+      $cgi = new CGI;
+      &cgisuidsetup($cgi);
+      #&cgisuidsetup($r);
+      $p = popurl(2);
+    }
+
+    $r->content_type('text/html');
+    #eorar
+
+    my $headers = $r->headers_out;
+    $headers->{'Pragma'} = $headers->{'Cache-control'} = 'no-cache';
+    #$r->no_cache(1);
+    $headers->{'Expires'} = '0';
+
+#    $r->send_http_header;
+
+    my $status = $ah->handle_request($r);
+
+    $status;
+}
+
+1;
diff --git a/httemplate/.htaccess b/httemplate/.htaccess
new file mode 100755 (executable)
index 0000000..f8c6b9c
--- /dev/null
@@ -0,0 +1,3 @@
+AuthName        Freeside
+AuthType        Basic
+require valid-user
diff --git a/httemplate/browse/addr_block.cgi b/httemplate/browse/addr_block.cgi
new file mode 100644 (file)
index 0000000..06ac556
--- /dev/null
@@ -0,0 +1,76 @@
+<%= header('Address Blocks', menubar('Main Menu'   => $p)) %>
+<%
+
+use NetAddr::IP;
+
+my @addr_block = qsearch('addr_block', {});
+my @router = qsearch('router', {});
+my $block;
+my $p2 = popurl(2);
+my $path = $p2 . "edit/process/addr_block";
+
+%>
+
+<% if ($cgi->param('error')) { %>
+   <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+   <BR><BR>
+<% } %>
+
+<%=table()%>
+
+<% foreach $block (sort {$a->NetAddr cmp $b->NetAddr} @addr_block) { %>
+  <TR>
+    <TD><%=$block->NetAddr%></TD>
+  <% if (my $router = $block->router) { %>
+    <% if (scalar($block->svc_broadband) == 0) { %>
+    <TD>
+      <%=$router->routername%>
+    </TD>
+    <TD>
+      <FORM ACTION="<%=$path%>/deallocate.cgi" METHOD="POST">
+        <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$block->blocknum%>">
+        <INPUT TYPE="submit" NAME="submit" VALUE="Deallocate">
+      </FORM>
+    </TD>
+    <% } else { %>
+    <TD COLSPAN="2">
+    <%=$router->routername%>
+    </TD>
+    <% } %>
+  <% } else { %>
+    <TD>
+      <FORM ACTION="<%=$path%>/allocate.cgi" METHOD="POST">
+        <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$block->blocknum%>">
+        <SELECT NAME="routernum" SIZE="1">
+    <% foreach (@router) { %>
+          <OPTION VALUE="<%=$_->routernum %>"><%=$_->routername%></OPTION>
+    <% } %>
+        </SELECT>
+        <INPUT TYPE="submit" NAME="submit" VALUE="Allocate">
+      </FORM>
+    </TD>
+    <TD>
+      <FORM ACTION="<%=$path%>/split.cgi" METHOD="POST">
+        <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$block->blocknum%>">
+        <INPUT TYPE="submit" NAME="submit" VALUE="Split">
+      </FORM>
+    </TD>
+  </TR>
+<% }
+ } %>
+  <TR><TD COLSPAN="3"><BR></TD></TR>
+  <TR>
+    <FORM ACTION="<%=$path%>/add.cgi" METHOD="POST">
+    <TD>Gateway/Netmask</TD>
+    <TD>
+      <INPUT TYPE="text" NAME="ip_gateway" SIZE="15">/<INPUT TYPE="text" NAME="ip_netmask" SIZE="2">
+    </TD>
+    <TD>
+      <INPUT TYPE="submit" NAME="submit" VALUE="Add">
+    </TD>
+    </FORM>
+  </TR>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/agent.cgi b/httemplate/browse/agent.cgi
new file mode 100755 (executable)
index 0000000..cff111c
--- /dev/null
@@ -0,0 +1,63 @@
+<!-- mason kludge -->
+<%
+#Begin silliness
+#
+#use FS::UI::CGI;
+#use FS::UI::agent;
+#
+#$ui = new FS::UI::agent;
+#$ui->browse;
+#exit;
+#__END__
+#End silliness
+%>
+
+<%= header('Agent Listing', menubar(
+  'Main Menu'   => $p,
+  'Agent Types' => $p. 'browse/agent_type.cgi',
+#  'Add new agent' => '../edit/agent.cgi'
+)) %>
+Agents are resellers of your service. Agents may be limited to a subset of your
+full offerings (via their type).<BR><BR>
+<A HREF="<%= $p %>edit/agent.cgi"><I>Add a new agent</I></A><BR><BR>
+
+<%= table() %>
+<TR>
+  <TH COLSPAN=2>Agent</TH>
+  <TH>Type</TH>
+  <TH><FONT SIZE=-1>Freq.</FONT></TH>
+  <TH><FONT SIZE=-1>Prog.</FONT></TH>
+</TR>
+<% 
+#        <TH><FONT SIZE=-1>Agent #</FONT></TH>
+#        <TH>Agent</TH>
+
+foreach my $agent ( sort { 
+  #$a->getfield('agentnum') <=> $b->getfield('agentnum')
+  $a->getfield('agent') cmp $b->getfield('agent')
+} qsearch('agent',{}) ) {
+  my($hashref)=$agent->hashref;
+  my($typenum)=$hashref->{typenum};
+  my($agent_type)=qsearchs('agent_type',{'typenum'=>$typenum});
+  my($atype)=$agent_type->getfield('atype');
+  print <<END;
+      <TR>
+        <TD><A HREF="${p}edit/agent.cgi?$hashref->{agentnum}">
+          $hashref->{agentnum}</A></TD>
+        <TD><A HREF="${p}edit/agent.cgi?$hashref->{agentnum}">
+          $hashref->{agent}</A></TD>
+        <TD><A HREF="${p}edit/agent_type.cgi?$typenum">$atype</A></TD>
+        <TD>$hashref->{freq}</TD>
+        <TD>$hashref->{prog}</TD>
+      </TR>
+END
+
+}
+
+print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/browse/agent_type.cgi b/httemplate/browse/agent_type.cgi
new file mode 100755 (executable)
index 0000000..5a84385
--- /dev/null
@@ -0,0 +1,58 @@
+<!-- mason kludge -->
+<%= header("Agent Type Listing", menubar(
+  'Main Menu' => $p,
+  'Agents'    => $p. 'browse/agent.cgi',
+)) %>
+Agent types define groups of packages that you can then assign to particular
+agents.<BR><BR>
+<A HREF="<%= $p %>edit/agent_type.cgi"><I>Add a new agent type</I></A><BR><BR>
+
+<%= table() %>
+<TR>
+  <TH COLSPAN=2>Agent Type</TH>
+  <TH COLSPAN=2>Packages</TH>
+</TR>
+
+<% 
+foreach my $agent_type ( sort { 
+  $a->getfield('typenum') <=> $b->getfield('typenum')
+} qsearch('agent_type',{}) ) {
+  my($hashref)=$agent_type->hashref;
+  my(@type_pkgs)=qsearch('type_pkgs',{'typenum'=> $hashref->{typenum} });
+  my($rowspan)=scalar(@type_pkgs);
+  $rowspan = int($rowspan/2+0.5) ;
+  print <<END;
+      <TR>
+        <TD ROWSPAN=$rowspan><A HREF="${p}edit/agent_type.cgi?$hashref->{typenum}">
+          $hashref->{typenum}
+        </A></TD>
+        <TD ROWSPAN=$rowspan><A HREF="${p}edit/agent_type.cgi?$hashref->{typenum}">$hashref->{atype}</A></TD>
+END
+
+  my($type_pkgs);
+  my($tdcount) = -1 ;
+  foreach $type_pkgs ( @type_pkgs ) {
+    my($pkgpart)=$type_pkgs->getfield('pkgpart');
+    my($part_pkg) = qsearchs('part_pkg',{'pkgpart'=> $pkgpart });
+    print qq!<TR>! if ($tdcount == 0) ;
+    $tdcount = 0 if ($tdcount == -1) ;
+    print qq!<TD><A HREF="${p}edit/part_pkg.cgi?$pkgpart">!,
+          $part_pkg->getfield('pkg'),"</A></TD>";
+    $tdcount ++ ;
+    if ($tdcount == 2)
+    {
+       print qq!</TR>\n! ;
+       $tdcount = 0 ;
+    }
+  }
+
+  print "</TR>";
+}
+
+print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/browse/cust_main_county.cgi b/httemplate/browse/cust_main_county.cgi
new file mode 100755 (executable)
index 0000000..c2473c4
--- /dev/null
@@ -0,0 +1,136 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+my $enable_taxclasses = $conf->exists('enable_taxclasses');
+
+print header("Tax Rate Listing", menubar(
+  'Main Menu' => $p,
+  'Edit tax rates' => $p. "edit/cust_main_county.cgi",
+)),<<END;
+    Click on <u>expand country</u> to specify a country's tax rates by state.
+    <BR>Click on <u>expand state</u> to specify a state's tax rates by county.
+END
+
+if ( $enable_taxclasses ) {
+  print '<BR>Click on <u>expand taxclasses</u> to specify tax classes';
+}
+
+print '<BR><BR>'. &table(). <<END;
+      <TR>
+        <TH><FONT SIZE=-1>Country</FONT></TH>
+        <TH><FONT SIZE=-1>State</FONT></TH>
+        <TH>County</TH>
+        <TH>Taxclass<BR><FONT SIZE=-1>(per-package classification)</FONT></TH>
+        <TH>Tax name<BR><FONT SIZE=-1>(printed on invoices)</FONT></TH>
+        <TH><FONT SIZE=-1>Tax</FONT></TH>
+        <TH><FONT SIZE=-1>Exempt<BR>per<BR>month</TH>
+      </TR>
+END
+
+my @regions = sort {    $a->country  cmp $b->country
+                     or $a->state    cmp $b->state
+                     or $a->county   cmp $b->county
+                     or $a->taxclass cmp $b->taxclass
+                   } qsearch('cust_main_county',{});
+
+my $sup=0;
+#foreach $cust_main_county ( @regions ) {
+for ( my $i=0; $i<@regions; $i++ ) { 
+  my $cust_main_county = $regions[$i];
+  my $hashref = $cust_main_county->hashref;
+  print <<END;
+      <TR>
+        <TD BGCOLOR="#ffffff">$hashref->{country}</TD>
+END
+
+  my $j;
+  if ( $sup ) {
+    $sup--;
+  } else {
+
+    #lookahead
+    for ( $j=1; $i+$j<@regions; $j++ ) {
+      last if $hashref->{country} ne $regions[$i+$j]->country
+           || $hashref->{state} ne $regions[$i+$j]->state
+           || $hashref->{tax} != $regions[$i+$j]->tax
+           || $hashref->{exempt_amount} != $regions[$i+$j]->exempt_amount;
+    }
+
+    my $newsup=0;
+    if ( $j>1 && $i+$j+1 < @regions
+         && ( $hashref->{state} ne $regions[$i+$j+1]->state 
+              || $hashref->{country} ne $regions[$i+$j+1]->country
+              )
+         && ( ! $i
+              || $hashref->{state} ne $regions[$i-1]->state 
+              || $hashref->{country} ne $regions[$i-1]->country
+              )
+       ) {
+       $sup = $j-1;
+    } else {
+      $j = 1;
+    }
+
+    print "<TD ROWSPAN=$j", $hashref->{state}
+        ? ' BGCOLOR="#ffffff">'. $hashref->{state}
+        : qq! BGCOLOR="#cccccc">(ALL) <FONT SIZE=-1>!.
+          qq!<A HREF="${p}edit/cust_main_county-expand.cgi?!. $hashref->{taxnum}.
+          qq!">expand country</A></FONT>!;
+
+    print qq! <FONT SIZE=-1><A HREF="${p}edit/process/cust_main_county-collapse.cgi?!. $hashref->{taxnum}. qq!">collapse state</A></FONT>! if $j>1;
+
+    print "</TD>";
+  }
+
+#  $sup=$newsup;
+
+  print "<TD";
+  if ( $hashref->{county} ) {
+    print ' BGCOLOR="#ffffff">'. $hashref->{county};
+  } else {
+    print ' BGCOLOR="#cccccc">(ALL)';
+    if ( $hashref->{state} ) {
+      print qq!<FONT SIZE=-1>!.
+          qq!<A HREF="${p}edit/cust_main_county-expand.cgi?!. $hashref->{taxnum}.
+          qq!">expand state</A></FONT>!;
+    }
+  }
+  print "</TD>";
+
+  print "<TD";
+  if ( $hashref->{taxclass} ) {
+    print ' BGCOLOR="#ffffff">'. $hashref->{taxclass};
+  } else {
+    print ' BGCOLOR="#cccccc">(ALL)';
+    if ( $enable_taxclasses ) {
+      print qq!<FONT SIZE=-1>!.
+            qq!<A HREF="${p}edit/cust_main_county-expand.cgi?taxclass!.
+            $hashref->{taxnum}. qq!">expand taxclasses</A></FONT>!;
+    }
+
+  }
+  print "</TD>";
+
+  print "<TD";
+  if ( $hashref->{taxname} ) {
+    print ' BGCOLOR="#ffffff">'. $hashref->{taxname};
+  } else {
+    print ' BGCOLOR="#cccccc">Tax';
+  }
+  print "</TD>";
+
+  print "<TD BGCOLOR=\"#ffffff\">$hashref->{tax}%</TD>".
+        '<TD BGCOLOR="#ffffff">$'.
+          sprintf("%.2f", $hashref->{exempt_amount} || 0). '</TD>'.
+        '</TR>';
+
+}
+
+print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/browse/cust_pay_batch.cgi b/httemplate/browse/cust_pay_batch.cgi
new file mode 100755 (executable)
index 0000000..608a58d
--- /dev/null
@@ -0,0 +1,52 @@
+<!-- mason kludge -->
+<%
+
+print header("Pending credit card batch", menubar(
+  'Main Menu' => $p,
+#  'Add new referral' => "../edit/part_referral.cgi",
+)), &table(), <<END;
+      <TR>
+        <TH>#</TH>
+        <TH><font size=-1>inv#</font></TH>
+        <TH COLSPAN=2>Customer</TH>
+        <TH>Card name</TH>
+        <TH>Card</TH>
+        <TH>Exp</TH>
+        <TH>Amount</TH>
+      </TR>
+END
+
+foreach my $cust_pay_batch ( sort { 
+  $a->getfield('paybatchnum') <=> $b->getfield('paybatchnum')
+} qsearch('cust_pay_batch',{}) ) {
+#  my $date = time2str( "%a %b %e %T %Y", $queue->_date );
+#  my $status = $hashref->{status};
+#  if ( $status eq 'failed' || $status eq 'locked' ) {
+#    $status .=
+#      qq! ( <A HREF="$p/edit/cust_pay_batch.cgi?jobnum=$jobnum&action=new">retry</A> |!.
+#      qq! <A HREF="$p/edit/cust_pay_batch.cgi?jobnum$jobnum&action=del">remove </A> )!;
+#  }
+  my $cardnum = $cust_pay_batch->{cardnum};
+  $cardnum =~ s/.{4}$/xxxx/;
+  print <<END;
+      <TR>
+        <TD>$cust_pay_batch->{paybatchnum}</TD>
+        <TD><A HREF="../view/cust_bill.cgi?$cust_pay_batch->{invnum}">$cust_pay_batch->{invnum}</TD>
+        <TD><A HREF="../view/cust_main.cgi?$cust_pay_batch->{custnum}">$cust_pay_batch->{custnum}</TD>
+        <TD>$cust_pay_batch->{last}, $cust_pay_batch->{last}</TD>
+        <TD>$cust_pay_batch->{payname}</TD>
+        <TD>$cardnum</TD>
+        <TD>$cust_pay_batch->{exp}</TD>
+        <TD align="right">\$$cust_pay_batch->{amount}</TD>
+      </TR>
+END
+
+}
+
+print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/browse/generic.cgi b/httemplate/browse/generic.cgi
new file mode 100644 (file)
index 0000000..9ac0f23
--- /dev/null
@@ -0,0 +1,46 @@
+<%
+
+use FS::Record qw(qsearch dbdef);
+use DBIx::DBSchema;
+use DBIx::DBSchema::Table;
+
+my $error;
+my $p2 = popurl(2);
+my ($table) = $cgi->keywords;
+my $dbdef = dbdef or die "Cannot fetch dbdef!";
+my $dbdef_table = $dbdef->table($table) or die "Cannot fetch schema for $table";
+
+my $pkey = $dbdef_table->primary_key or die "Cannot fetch pkey for $table";
+print header("Browse $table", menubar('Main Menu'   => $p));
+
+my @rec = qsearch($table, {});
+my @col = $dbdef_table->columns;
+
+if ($cgi->param('error')) { %>
+   <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+   <BR><BR>
+<% } 
+%>
+<A HREF="<%=$p2%>edit/<%=$table%>.cgi"><I>Add a new <%=$table%></I></A><BR><BR>
+
+<%=table()%>
+<TH>
+<% foreach (grep { $_ ne $pkey } @col) {
+  %><TD><%=$_%></TD>
+  <% } %>
+</TH>
+<% foreach $rec (sort {$a->getfield($pkey) cmp $b->getfield($pkey) } @rec) { 
+  %>
+  <TR>
+    <TD>
+      <A HREF="<%=$p2%>edit/<%=$table%>.cgi?<%=$rec->getfield($pkey)%>">
+      <%=$rec->getfield($pkey)%></A> </TD> <%
+  foreach $col (grep { $_ ne $pkey } @col)  { %>
+    <TD><%=$rec->getfield($col)%></TD> <% } %>
+  </A>
+  </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/msgcat.cgi b/httemplate/browse/msgcat.cgi
new file mode 100755 (executable)
index 0000000..d4adf9f
--- /dev/null
@@ -0,0 +1,50 @@
+<!-- mason kludge -->
+<%
+
+print header("View Message catalog", menubar(
+  'Main Menu' => $p,
+  'Edit message catalog' => $p. "edit/msgcat.cgi",
+)), '<BR>';
+
+my $widget = new HTML::Widgets::SelectLayers(
+  'selected_layer' => 'en_US',
+  'options'        => { 'en_US'=>'en_US' },
+  'layer_callback' => sub {
+    my $layer = shift;
+    my $html = "<BR>Messages for locale $layer<BR>". table().
+               "<TR><TH COLSPAN=2>Code</TH>".
+               "<TH>Message</TH>";
+    $html .= "<TH>en_US Message</TH>" unless $layer eq 'en_US';
+    $html .= '</TR>';
+
+    #foreach my $msgcat ( sort { $a->msgcode cmp $b->msgcode }
+    #                       qsearch('msgcat', { 'locale' => $layer } ) ) {
+    foreach my $msgcat ( qsearch('msgcat', { 'locale' => $layer } ) ) {
+      $html .= '<TR><TD>'. $msgcat->msgnum. '</TD>'.
+               '<TD>'. $msgcat->msgcode. '</TD>'.
+               '<TD>'. $msgcat->msg. '</TD>';
+      unless ( $layer eq 'en_US' ) {
+        my $en_msgcat = qsearchs('msgcat', {
+          'locale'  => 'en_US',
+          'msgcode' => $msgcat->msgcode,
+        } );
+        $html .= '<TD>'. $en_msgcat->msg. '</TD>';
+      }
+      $html .= '</TR>';
+    }
+
+    $html .= '</TABLE>';
+    $html;
+  },
+
+);
+
+print $widget->html;
+
+print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/browse/nas.cgi b/httemplate/browse/nas.cgi
new file mode 100755 (executable)
index 0000000..9ccbfe6
--- /dev/null
@@ -0,0 +1,80 @@
+<!-- mason kludge -->
+<%
+
+print header('NAS ports', menubar(
+  'Main Menu' => $p,
+));
+
+my $now = time;
+
+foreach my $nas ( sort { $a->nasnum <=> $b->nasnum } qsearch( 'nas', {} ) ) {
+  print $nas->nasnum. ": ". $nas->nas. " ".
+        $nas->nasfqdn. " (". $nas->nasip. ") ".
+        "as of ". time2str("%c",$nas->last).
+        " (". &pretty_interval($now - $nas->last). " ago)<br>".
+        &table(). "<TR><TH>Nas<BR>Port #</TH><TH>Global<BR>Port #</BR></TH>".
+        "<TH>IP address</TH><TH>User</TH><TH>Since</TH><TH>Duration</TH><TR>",
+  ;
+  foreach my $port ( sort {
+    $a->nasport <=> $b->nasport || $a->portnum <=> $b->portnum
+  } qsearch( 'port', { 'nasnum' => $nas->nasnum } ) ) {
+    my $session = $port->session;
+    my($user, $since, $pretty_since, $duration);
+    if ( ! $session ) {
+      $user = "(empty)";
+      $since = 0;
+      $pretty_since = "(never)";
+      $duration = '';
+    } elsif ( $session->logout ) {
+      $user = "(empty)";
+      $since = $session->logout;
+    } else {
+      my $svc_acct = $session->svc_acct;
+      $user = "<A HREF=\"$p/view/svc_acct.cgi?". $svc_acct->svcnum. "\">".
+              $svc_acct->username. "</A>";
+      $since = $session->login;
+    }
+    $pretty_since = time2str("%c", $since) if $since;
+    $duration = pretty_interval( $now - $since ). " ago"
+      unless defined($duration);
+    print "<TR><TD>". $port->nasport. "</TD><TD>". $port->portnum. "</TD><TD>".
+          $port->ip. "</TD><TD>$user</TD><TD>$pretty_since".
+          "</TD><TD>$duration</TD></TR>"
+    ;
+  }
+  print "</TABLE><BR>";
+}
+
+#Time::Duration??
+sub pretty_interval {
+  my $interval = shift;
+  my %howlong = (
+    '604800' => 'week',
+    '86400'  => 'day',
+    '3600'   => 'hour',
+    '60'     => 'minute',
+    '1'      => 'second',
+  );
+
+  my $pretty = "";
+  foreach my $key ( sort { $b <=> $a } keys %howlong ) {
+    my $value = int( $interval / $key );
+    if ( $value  ) {
+      if ( $value == 1 ) {
+        $pretty .=
+          ( $howlong{$key} eq 'hour' ? 'an ' : 'a ' ). $howlong{$key}. " "
+      } else {
+        $pretty .= $value. ' '. $howlong{$key}. 's ';
+      }
+    }
+    $interval -= $value * $key;
+  }
+  $pretty =~ /^\s*(\S.*\S)\s*$/;
+  $1;
+} 
+
+#print &table(), <<END;
+#<TR>
+#  <TH>#</TH>
+#  <TH>NAS</
+%>
diff --git a/httemplate/browse/part_bill_event.cgi b/httemplate/browse/part_bill_event.cgi
new file mode 100755 (executable)
index 0000000..670474d
--- /dev/null
@@ -0,0 +1,71 @@
+<!-- mason kludge -->
+<% 
+
+my %search;
+if ( $cgi->param('showdisabled') ) {
+  %search = ();
+} else {
+  %search = ( 'disabled' => '' );
+}
+
+my @part_bill_event = qsearch('part_bill_event', \%search );
+my $total = scalar(@part_bill_event);
+
+%>
+<%= header('Invoice Event Listing', menubar( 'Main Menu' => $p) ) %>
+
+    Invoice events are actions taken on overdue invoices.<BR><BR>
+<A HREF="<%= $p %>edit/part_bill_event.cgi"><I>Add a new invoice event</I></A>
+<BR><BR>
+<%= $total %> events
+<%= $cgi->param('showdisabled')
+      ? do { $cgi->param('showdisabled', 0);
+             '( <a href="'. $cgi->self_url. '">hide disabled events</a> )'; }
+      : do { $cgi->param('showdisabled', 1);
+             '( <a href="'. $cgi->self_url. '">show disabled events</a> )'; }
+%>
+<%= table() %>
+  <TR>
+    <TH COLSPAN=<%= $cgi->param('showdisabled') ? 2 : 3 %>>Event</TH>
+    <TH>Payby</TH>
+    <TH>After</TH>
+    <TH>Action</TH>
+    <TH>Options</TH>
+    <TH>Code</TH>
+  </TR>
+
+<% foreach my $part_bill_event ( sort {    $a->payby     cmp $b->payby
+                                        || $a->seconds   <=> $b->seconds
+                                        || $a->weight    <=> $b->weight
+                                        || $a->eventpart <=> $b->eventpart
+                                      } @part_bill_event ) {
+     my $url = "${p}edit/part_bill_event.cgi?". $part_bill_event->eventpart;
+     use Time::Duration;
+     my $delay = duration_exact($part_bill_event->seconds);
+     my $plandata = $part_bill_event->plandata;
+     $plandata =~ s/\n/<BR>/go;
+%>
+  <TR>
+    <TD><A HREF="<%= $url %>">
+      <%= $part_bill_event->eventpart %></A></TD>
+<% unless ( $cgi->param('showdisabled') ) { %>
+    <TD>
+      <%= $part_bill_event->disabled ? 'DISABLED' : '' %></TD>
+<% } %>
+    <TD><A HREF="<%= $url %>">
+      <%= $part_bill_event->event %></A></TD>
+    <TD>
+      <%= $part_bill_event->payby %></TD>
+    <TD>
+      <%= $delay %></TD>
+    <TD>
+      <%= $part_bill_event->plan %></TD>
+    <TD>
+      <%= $plandata %></TD>
+    <TD><FONT SIZE="-1">
+      <%= $part_bill_event->eventcode %></FONT></TD>
+  </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
diff --git a/httemplate/browse/part_export.cgi b/httemplate/browse/part_export.cgi
new file mode 100755 (executable)
index 0000000..76662e0
--- /dev/null
@@ -0,0 +1,39 @@
+<!-- mason kludge -->
+<%= header("Export Listing", menubar( 'Main Menu' => "$p#sysadmin" )) %>
+Provisioning services to external machines, databases and APIs.<BR><BR>
+<A HREF="<%= $p %>edit/part_export.cgi"><I>Add a new export</I></A><BR><BR>
+<SCRIPT>
+function part_export_areyousure(href) {
+  if (confirm("Are you sure you want to delete this export?") == true)
+    window.location.href = href;
+}
+</SCRIPT>
+
+<%= table() %>
+  <TR>
+    <TH COLSPAN=2>Export</TH>
+    <TH>Options</TH>
+  </TR>
+
+<% foreach my $part_export ( sort { 
+     $a->getfield('exportnum') <=> $b->getfield('exportnum')
+   } qsearch('part_export',{}) ) {
+%>
+  <TR>
+    <TD><A HREF="<%= $p %>edit/part_export.cgi?<%= $part_export->exportnum %>"><%= $part_export->exportnum %></A></TD>
+    <TD><%= $part_export->exporttype %> to <%= $part_export->machine %> (<A HREF="<%= $p %>edit/part_export.cgi?<%= $part_export->exportnum %>">edit</A>&nbsp;|&nbsp;<A HREF="javascript:part_export_areyousure('<%= $p %>misc/delete-part_export.cgi?<%= $part_export->exportnum %>')">delete</A>)</TD>
+    <TD>
+      <%= itable() %>
+      <% my %opt = $part_export->options;
+         foreach my $opt ( keys %opt ) { %>
+           <TR><TD><%= $opt %></TD><TD><%= $opt{$opt} %></TD></TR>
+      <% } %>
+      </TABLE>
+    </TD>
+  </TR>
+
+<% } %>
+
+</TABLE>
+</BODY>
+</HTML>
diff --git a/httemplate/browse/part_pkg.cgi b/httemplate/browse/part_pkg.cgi
new file mode 100755 (executable)
index 0000000..7b9436c
--- /dev/null
@@ -0,0 +1,144 @@
+<!-- mason kludge -->
+<%
+
+my %search;
+if ( $cgi->param('showdisabled') ) {
+  %search = ();
+} else {
+  %search = ( 'disabled' => '' );
+}
+
+my @part_pkg = qsearch('part_pkg', \%search );
+my $total = scalar(@part_pkg);
+
+my $sortby;
+my %num_active_cust_pkg;
+if ( $cgi->param('active') ) {
+  my $active_sth = dbh->prepare(
+    'SELECT COUNT(*) FROM cust_pkg WHERE pkgpart = ?'.
+    ' AND ( cancel IS NULL OR cancel = 0 )'.
+    ' AND ( susp IS NULL OR susp = 0 )'
+  ) or die dbh->errstr;
+  foreach my $part_pkg ( @part_pkg ) {
+    $active_sth->execute($part_pkg->pkgpart) or die $active_sth->errstr;
+    $num_active_cust_pkg{$part_pkg->pkgpart} =
+      $active_sth->fetchrow_arrayref->[0];
+  }
+  $sortby = \*active_cust_pkg_sort;
+} else {
+  $sortby = \*pkgpart_sort;
+}
+
+%>
+<%= header("Package Definition Listing",menubar( 'Main Menu' => $p )) %>
+<% unless ( $cgi->param('active') ) { %>
+  One or more service definitions are grouped together into a package 
+  definition and given pricing information.  Customers purchase packages
+  rather than purchase services directly.<BR><BR>
+  <A HREF="<%= $p %>edit/part_pkg.cgi"><I>Add a new package definition</I></A>
+  <BR><BR>
+<% } %>
+
+<%= $total %> package definitions
+<%
+if ( $cgi->param('showdisabled') ) {
+  $cgi->param('showdisabled', 0);
+  print qq!( <a href="!. $cgi->self_url. qq!">hide disabled packages</a> )!;
+} else {
+  $cgi->param('showdisabled', 1);
+  print qq!( <a href="!. $cgi->self_url. qq!">show disabled packages</a> )!;
+}
+
+my $colspan = $cgi->param('showdisabled') ? 2 : 3;
+print &table(), <<END;
+      <TR>
+        <TH COLSPAN=$colspan>Package</TH>
+        <TH>Comment</TH>
+END
+print '        <TH><FONT SIZE=-1>Customer<BR>packages</FONT></TH>'
+  if $cgi->param('active');
+print <<END;
+        <TH><FONT SIZE=-1>Freq.</FONT></TH>
+        <TH><FONT SIZE=-1>Plan</FONT></TH>
+        <TH><FONT SIZE=-1>Data</FONT></TH>
+        <TH>Service</TH>
+        <TH><FONT SIZE=-1>Quan.</FONT></TH>
+      </TR>
+END
+
+foreach my $part_pkg ( sort $sortby @part_pkg ) {
+  my($hashref)=$part_pkg->hashref;
+  my(@pkg_svc)=grep $_->getfield('quantity'),
+    qsearch('pkg_svc',{'pkgpart'=> $hashref->{pkgpart} });
+  my($rowspan)=scalar(@pkg_svc);
+  my $plandata;
+  if ( $hashref->{plan} ) {
+    $plandata = $hashref->{plandata};
+    $plandata =~ s/^(\w+)=/$1&nbsp;/mg;
+    $plandata =~ s/\n/<BR>/g;
+  } else {
+    $hashref->{plan} = "(legacy)";
+    $plandata = "Setup&nbsp;". $hashref->{setup}.
+                "<BR>Recur&nbsp;". $hashref->{recur};
+  }
+  print <<END;
+      <TR>
+        <TD ROWSPAN=$rowspan><A HREF="${p}edit/part_pkg.cgi?$hashref->{pkgpart}">$hashref->{pkgpart}</A></TD>
+END
+
+  unless ( $cgi->param('showdisabled') ) {
+    print "<TD ROWSPAN=$rowspan>";
+    print "DISABLED" if $hashref->{disabled};
+    print '</TD>';
+  }
+
+  print <<END;
+        <TD ROWSPAN=$rowspan><A HREF="${p}edit/part_pkg.cgi?$hashref->{pkgpart}">$hashref->{pkg}</A></TD>
+        <TD ROWSPAN=$rowspan>$hashref->{comment}</TD>
+END
+  if ( $cgi->param('active') ) {
+    print "        <TD ROWSPAN=$rowspan>";
+    print '<FONT COLOR="#00CC00"><B>'.
+          $num_active_cust_pkg{$hashref->{'pkgpart'}}.
+          qq!</B></FONT>&nbsp;<A HREF="${p}search/cust_pkg.cgi?magic=active;pkgpart=$hashref->{pkgpart}">active</A>!;
+    # suspended/cancelled
+    print '</TD>';
+  }
+  print <<END;
+        <TD ROWSPAN=$rowspan>$hashref->{freq}</TD>
+        <TD ROWSPAN=$rowspan>$hashref->{plan}</TD>
+        <TD ROWSPAN=$rowspan>$plandata</TD>
+END
+
+  my($pkg_svc);
+  my($n)="";
+  foreach $pkg_svc ( @pkg_svc ) {
+    my($svcpart)=$pkg_svc->getfield('svcpart');
+    my($part_svc) = qsearchs('part_svc',{'svcpart'=> $svcpart });
+    print $n,qq!<TD><A HREF="${p}edit/part_svc.cgi?$svcpart">!,
+          $part_svc->getfield('svc'),"</A></TD><TD>",
+          $pkg_svc->getfield('quantity'),"</TD></TR>\n";
+    $n="<TR>";
+  }
+
+  print "</TR>";
+}
+
+$colspan = $cgi->param('showdisabled') ? 8 : 9;
+print <<END;
+
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+
+sub pkgpart_sort {
+  $a->pkgpart <=> $b->pkgpart;
+}
+
+sub active_cust_pkg_sort {
+  $num_active_cust_pkg{$b->pkgpart} <=> $num_active_cust_pkg{$a->pkgpart};
+}
+
+%>
diff --git a/httemplate/browse/part_referral.cgi b/httemplate/browse/part_referral.cgi
new file mode 100755 (executable)
index 0000000..084c21b
--- /dev/null
@@ -0,0 +1,38 @@
+<!-- mason kludge -->
+<%= header("Advertising source Listing", menubar(
+  'Main Menu' => $p,
+#  'Add new referral' => "../edit/part_referral.cgi",
+)) %>
+Where a customer heard about your service. Tracked for informational purposes.
+<BR><BR>
+<A HREF="<%= $p %>edit/part_referral.cgi"><I>Add a new advertising source</I></A>
+<BR><BR>
+
+<%= table() %>
+<TR>
+  <TH COLSPAN=2>Advertising source</TH>
+</TR>
+
+<%
+foreach my $part_referral ( sort { 
+  $a->getfield('refnum') <=> $b->getfield('refnum')
+} qsearch('part_referral',{}) ) {
+  my($hashref)=$part_referral->hashref;
+  print <<END;
+      <TR>
+        <TD><A HREF="${p}edit/part_referral.cgi?$hashref->{refnum}">
+          $hashref->{refnum}</A></TD>
+        <TD><A HREF="${p}edit/part_referral.cgi?$hashref->{refnum}">
+          $hashref->{referral}</A></TD>
+      </TR>
+END
+
+}
+
+print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/browse/part_sb_field.cgi b/httemplate/browse/part_sb_field.cgi
new file mode 100644 (file)
index 0000000..4c9641e
--- /dev/null
@@ -0,0 +1,31 @@
+<%= header('svc_broadband extended fields', menubar('Main Menu'   => $p)) %>
+<%
+
+my @psf = qsearch('part_sb_field', {});
+my $block;
+my $p2 = popurl(2);
+
+%>
+
+<% if ($cgi->param('error')) { %>
+   <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+   <BR><BR>
+<% } %>
+
+<A HREF="<%=$p2%>edit/part_sb_field.cgi"><I>Add a new field</I></A><BR><BR>
+
+<%=table()%>
+<TH><TD>Field name</TD><TD>Service type</TD></TH>
+<% foreach $psf (sort {$a->name cmp $b->name} @psf) { %>
+  <TR>
+    <TD></TD>
+    <TD>
+      <A HREF="<%=$p2%>edit/part_sb_field.cgi?<%=$psf->sbfieldpart%>">
+        <%=$psf->name%></A></TD>
+    <TD><%=$psf->part_svc->svc%></TD>
+  </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/part_svc.cgi b/httemplate/browse/part_svc.cgi
new file mode 100755 (executable)
index 0000000..7c83924
--- /dev/null
@@ -0,0 +1,109 @@
+<!-- mason kludge -->
+<% 
+
+my %search;
+if ( $cgi->param('showdisabled') ) {
+  %search = ();
+} else {
+  %search = ( 'disabled' => '' );
+}
+
+my @part_svc =
+  sort { $a->getfield('svcpart') <=> $b->getfield('svcpart') }
+    qsearch('part_svc', \%search );
+my $total = scalar(@part_svc);
+
+%>
+<%= header('Service Definition Listing', menubar( 'Main Menu' => $p) ) %>
+
+<SCRIPT>
+function part_export_areyousure(href) {
+  if (confirm("Are you sure you want to delete this export?") == true)
+    window.location.href = href;
+}
+</SCRIPT>
+
+    Service definitions are the templates for items you offer to your customers.<BR><BR>
+
+<FORM METHOD="POST" ACTION="<%= $p %>edit/part_svc.cgi">
+<A HREF="<%= $p %>edit/part_svc.cgi"><I>Add a new service definition</I></A><% if ( @part_svc ) { %>&nbsp;or&nbsp;<SELECT NAME="clone"><OPTION></OPTION>
+<% foreach my $part_svc ( @part_svc ) { %>
+  <OPTION VALUE="<%= $part_svc->svcpart %>"><%= $part_svc->svc %></OPTION>
+<% } %>
+</SELECT><INPUT TYPE="submit" VALUE="Clone existing service">
+<% } %>
+</FORM><BR>
+
+<%= $total %> service definitions
+<%= $cgi->param('showdisabled')
+      ? do { $cgi->param('showdisabled', 0);
+             '( <a href="'. $cgi->self_url. '">hide disabled services</a> )'; }
+      : do { $cgi->param('showdisabled', 1);
+             '( <a href="'. $cgi->self_url. '">show disabled services</a> )'; }
+%>
+<%= table() %>
+  <TR>
+    <TH COLSPAN=<%= $cgi->param('showdisabled') ? 2 : 3 %>>Service</TH>
+    <TH>Table</TH>
+    <TH>Export</TH>
+    <TH>Field</TH>
+    <TH COLSPAN=2>Modifier</TH>
+  </TR>
+
+<% foreach my $part_svc ( @part_svc ) {
+     my $hashref = $part_svc->hashref;
+     my $svcdb = $hashref->{svcdb};
+     my @dfields = fields($svcdb);
+     push @dfields, 'usergroup' if $svcdb eq 'svc_acct'; #kludge
+     my @fields =
+       grep { $_ ne 'svcnum' && $part_svc->part_svc_column($_)->columnflag }
+            @dfields;
+
+     my $rowspan = scalar(@fields) || 1;
+     my $url = "${p}edit/part_svc.cgi?$hashref->{svcpart}";
+%>
+
+  <TR>
+    <TD ROWSPAN=<%= $rowspan %>><A HREF="<%= $url %>">
+      <%= $hashref->{svcpart} %></A></TD>
+<% unless ( $cgi->param('showdisabled') ) { %>
+    <TD ROWSPAN=<%= $rowspan %>>
+      <%= $hashref->{disabled} ? 'DISABLED' : '' %></TD>
+<% } %>
+    <TD ROWSPAN=<%= $rowspan %>><A HREF="<%= $url %>">
+      <%= $hashref->{svc} %></A></TD>
+    <TD ROWSPAN=<%= $rowspan %>>
+      <%= $hashref->{svcdb} %></TD>
+    <TD ROWSPAN=<%= $rowspan %>><%= itable() %>
+<%
+#  my @part_export =
+map { qsearchs('part_export', { exportnum => $_->exportnum } ) } qsearch('export_svc', { svcpart => $part_svc->svcpart } ) ;
+  foreach my $part_export (
+    map { qsearchs('part_export', { exportnum => $_->exportnum } ) } 
+      qsearch('export_svc', { svcpart => $part_svc->svcpart } )
+  ) {
+%>
+      <TR>
+        <TD><A HREF="<%= $p %>edit/part_export.cgi?<%= $part_export->exportnum %>"><%= $part_export->exportnum %>:&nbsp;<%= $part_export->exporttype %>&nbsp;to&nbsp;<%= $part_export->machine %></A></TD></TR>
+<%  } %>
+      </TABLE></TD>
+
+<%   my($n1)='';
+     foreach my $field ( @fields ) {
+       my $flag = $part_svc->part_svc_column($field)->columnflag;
+%>
+     <%= $n1 %><TD><%= $field %></TD><TD>
+
+<%     if ( $flag eq "D" ) { print "Default"; }
+         elsif ( $flag eq "F" ) { print "Fixed"; }
+         else { print "(Unknown!)"; }
+%>
+       </TD><TD><%= $part_svc->part_svc_column($field)->columnvalue%></TD>
+<%     $n1="</TR><TR>";
+     }
+%>
+  </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
diff --git a/httemplate/browse/queue.cgi b/httemplate/browse/queue.cgi
new file mode 100755 (executable)
index 0000000..b53c140
--- /dev/null
@@ -0,0 +1,7 @@
+<!-- mason kludge -->
+<%
+
+print header("Job Queue", menubar( 'Main Menu' => $p, )).
+      joblisting({}). '</BODY></HTML>';
+
+%>
diff --git a/httemplate/browse/router.cgi b/httemplate/browse/router.cgi
new file mode 100644 (file)
index 0000000..8864936
--- /dev/null
@@ -0,0 +1,37 @@
+<%= header('Routers', menubar('Main Menu'   => $p)) %>
+<%
+
+my @router = qsearch('router', {});
+my $p2 = popurl(2);
+
+%>
+
+<% if ($cgi->param('error')) { %>
+   <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+   <BR><BR>
+<% } %>
+
+<A HREF="<%=$p2%>edit/router.cgi"><I>Add a new router</I></A><BR><BR>
+
+<%=table()%>
+<!-- <TH><TD>Field name</TD><TD>Field value</TD></TH> -->
+<% foreach $router (sort {$a->routernum <=> $b->routernum} @router) { %>
+  <TR>
+<!--    <TD ROWSPAN="<%=scalar($router->router_field) + 2%>"> -->
+    <TD>
+      <A HREF="<%=$p2%>edit/router.cgi?<%=$router->routernum%>"><%=$router->routername%></A>
+    </TD>
+  <!-- 
+  <% foreach (sort { $a->part_router_field->name cmp $b->part_router_field->name } $router->router_field )  { %>
+  <TR>
+    <TD BGCOLOR="#cccccc" ALIGN="right"><%=$_->part_router_field->name%></TD>
+    <TD BGCOLOR="#ffffff"><%=$_->value%></TD>
+  </TR>
+  <% } %>
+  -->
+  </TR>
+<% } %>
+</TABLE>
+</BODY>
+</HTML>
+
diff --git a/httemplate/browse/svc_acct_pop.cgi b/httemplate/browse/svc_acct_pop.cgi
new file mode 100755 (executable)
index 0000000..8d35cb5
--- /dev/null
@@ -0,0 +1,49 @@
+<!-- mason kludge -->
+<%= header('Access Number Listing', menubar( 'Main Menu' => $p )) %>
+Points of Presence<BR><BR>
+<A HREF="<%= $p %>edit/svc_acct_pop.cgi"><I>Add new Access Number</I></A><BR><BR>
+<%= table() %>
+      <TR>
+        <TH></TH>
+        <TH>City</TH>
+        <TH>State</TH>
+        <TH>Area code</TH>
+        <TH>Exchange</TH>
+        <TH>Local</TH>
+      </TR>
+
+<%
+foreach my $svc_acct_pop ( sort { 
+  #$a->getfield('popnum') <=> $b->getfield('popnum')
+  $a->state cmp $b->state || $a->city cmp $b->city
+    || $a->ac <=> $b->ac || $a->exch <=> $b->exch || $a->loc <=> $b->loc
+} qsearch('svc_acct_pop',{}) ) {
+  my($hashref)=$svc_acct_pop->hashref;
+  print <<END;
+      <TR>
+        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
+          $hashref->{popnum}</A></TD>
+        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
+          $hashref->{city}</A></TD>
+        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
+          $hashref->{state}</A></TD>
+        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
+          $hashref->{ac}</A></TD>
+        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
+          $hashref->{exch}</A></TD>
+        <TD><A HREF="${p}edit/svc_acct_pop.cgi?$hashref->{popnum}">
+          $hashref->{loc}</A></TD>
+      </TR>
+END
+
+}
+
+print <<END;
+      <TR>
+      </TR>
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/config/config-process.cgi b/httemplate/config/config-process.cgi
new file mode 100644 (file)
index 0000000..2597132
--- /dev/null
@@ -0,0 +1,51 @@
+<%
+  my $conf = new FS::Conf;
+  $FS::Conf::DEBUG = 1;
+  my @config_items = $conf->config_items;
+
+  foreach my $i ( @config_items ) {
+    my @touch = ();
+    my @delete = ();
+    my $n = 0;
+    foreach my $type ( ref($i->type) ? @{$i->type} : $i->type ) {
+      if ( $type eq '' ) {
+      } elsif ( $type eq 'textarea' ) {
+        if ( $cgi->param($i->key. $n) ne '' ) {
+          my $value = $cgi->param($i->key. $n);
+          $value =~ s/\r\n/\n/g; #browsers?
+          $conf->set($i->key, $value);
+        } else {
+          $conf->delete($i->key);
+        }
+      } elsif ( $type eq 'checkbox' ) {
+#        if ( defined($cgi->param($i->key. $n)) && $cgi->param($i->key. $n) ) {
+        if ( defined $cgi->param($i->key. $n) ) {
+          #$conf->touch($i->key);
+          push @touch, $i->key;
+        } else {
+          #$conf->delete($i->key);
+          push @delete, $i->key;
+        }
+      } elsif ( $type eq 'text' || $type eq 'select' )  {
+        if ( $cgi->param($i->key. $n) ne '' ) {
+          $conf->set($i->key, $cgi->param($i->key. $n));
+        } else {
+          $conf->delete($i->key);
+        }
+      } elsif ( $type eq 'editlist' || $type eq 'selectmultiple' )  {
+        if ( scalar(@{[ $cgi->param($i->key. $n) ]}) ) {
+          $conf->set($i->key, join("\n", @{[ $cgi->param($i->key. $n) ]} ));
+        } else {
+          $conf->delete($i->key);
+        }
+      } else {
+      }
+      $n++;
+    }
+   # warn @touch;
+    $conf->touch($_) foreach @touch;
+    $conf->delete($_) foreach @delete;
+  }
+
+%>
+<%= $cgi->redirect("config-view.cgi") %>
diff --git a/httemplate/config/config-view.cgi b/httemplate/config/config-view.cgi
new file mode 100644 (file)
index 0000000..9a00067
--- /dev/null
@@ -0,0 +1,64 @@
+<!-- mason kludge -->
+<%= header('View Configuration', menubar( 'Main Menu' => $p,
+                                     'Edit Configuration' => 'config.cgi' ) ) %>
+
+<% my $conf = new FS::Conf; my @config_items = $conf->config_items; %>
+
+<% foreach my $section ( qw(required billing username password UI session
+                            shell BIND
+                           ),
+                         '', 'deprecated') { %>
+  <A NAME="<%= $section || 'unclassified' %>"></A>
+  <FONT SIZE="-2">
+  <% foreach my $nav_section ( qw(required billing username password UI session
+                                  shell BIND
+                                 ),
+                               '', 'deprecated') { %>
+    <% if ( $section eq $nav_section ) { %>
+      [<A NAME="not<%= $nav_section || 'unclassified' %>" style="background-color: #cccccc"><%= ucfirst($nav_section || 'unclassified') %></A>]
+    <% } else { %>
+      [<A HREF="#<%= $nav_section || 'unclassified' %>"><%= ucfirst($nav_section || 'unclassified') %></A>]
+    <% } %>
+  <% } %>
+  </FONT><BR>
+  <%= table("#cccccc", 2) %>
+  <tr>
+    <th colspan="2" bgcolor="#dcdcdc">
+      <%= ucfirst($section || 'unclassified') %> configuration options
+    </th>
+  </tr>
+  <% foreach my $i (grep $_->section eq $section, @config_items) { %>
+    <tr>
+      <td><a name="<%= $i->key %>">
+        <b><%= $i->key %></b>&nbsp;-&nbsp;<%= $i->description %>
+      </a></td>
+      <td><table border=0>
+        <% foreach my $type ( ref($i->type) ? @{$i->type} : $i->type ) {
+             my $n = 0; %>
+          <% if ( $type eq '' ) { %>
+            <tr><td><font color="#ff0000">no type</font></td></tr>
+          <% } elsif (   $type eq 'textarea'
+                      || $type eq 'editlist'
+                      || $type eq 'selectmultiple' ) { %>
+            <tr><td bgcolor="#ffffff">
+<pre>
+<%= encode_entities(join("\n", $conf->config($i->key) ) ) %>
+</pre>
+            </td></tr>
+          <% } elsif ( $type eq 'checkbox' ) { %>
+            <tr><td bgcolor="#<%= $conf->exists($i->key) ? '00ff00">YES' : 'ff0000">NO' %></td></tr>
+          <% } elsif ( $type eq 'text' || $type eq 'select' )  { %>
+            <tr><td bgcolor="#ffffff"><%= $conf->exists($i->key) ? $conf->config($i->key) : '' %></td></tr>
+          <% } else { %>
+            <tr><td>
+              <font color="#ff0000">unknown type <%= $type %></font>
+            </td></tr>
+          <% } %>
+        <% $n++; } %>
+      </table></td>
+    </tr>
+  <% } %>
+  </table><br><br>
+<% } %>
+
+</body></html>
diff --git a/httemplate/config/config.cgi b/httemplate/config/config.cgi
new file mode 100644 (file)
index 0000000..409869e
--- /dev/null
@@ -0,0 +1,176 @@
+<!-- mason kludge -->
+<%= header('Edit Configuration', menubar( 'Main Menu' => $p ) ) %>
+<SCRIPT>
+var gSafeOnload = new Array();
+var gSafeOnsubmit = new Array();
+window.onload = SafeOnload;
+function SafeAddOnLoad(f) {
+  gSafeOnload[gSafeOnload.length] = f;
+}
+function SafeOnload() {
+  for (var i=0;i<gSafeOnload.length;i++)
+    gSafeOnload[i]();
+}
+function SafeAddOnSubmit(f) {
+  gSafeOnsubmit[gSafeOnsubmit.length] = f;
+}
+function SafeOnsubmit() {
+  for (var i=0;i<gSafeOnsubmit.length;i++)
+    gSafeOnsubmit[i]();
+}
+</SCRIPT>
+
+<% my $conf = new FS::Conf; my @config_items = $conf->config_items; %>
+
+<form name="OneTrueForm" action="config-process.cgi" METHOD="POST" onSubmit="SafeOnsubmit()">
+
+<% foreach my $section ( qw(required billing username password UI session
+                            shell BIND
+                           ),
+                         '', 'deprecated') { %>
+  <A NAME="<%= $section || 'unclassified' %>"></A>
+  <FONT SIZE="-2">
+  <% foreach my $nav_section ( qw(required billing username password UI session
+                                  shell BIND
+                                 ),
+                               '', 'deprecated') { %>
+    <% if ( $section eq $nav_section ) { %>
+      [<A NAME="not<%= $nav_section || 'unclassified' %>" style="background-color: #cccccc"><%= ucfirst($nav_section || 'unclassified') %></A>]
+    <% } else { %>
+      [<A HREF="#<%= $nav_section || 'unclassified' %>"><%= ucfirst($nav_section || 'unclassified') %></A>]
+    <% } %>
+  <% } %>
+  </FONT><BR>
+  <%= table("#cccccc", 2) %>
+  <tr>
+    <th colspan="2" bgcolor="#dcdcdc">
+      <%= ucfirst($section || 'unclassified') %> configuration options
+    </th>
+  </tr>
+  <% foreach my $i (grep $_->section eq $section, @config_items) { %>
+    <tr>
+      <td>
+        <% my $n = 0;
+           foreach my $type ( ref($i->type) ? @{$i->type} : $i->type ) {
+             #warn $i->key unless defined($type);
+        %>
+          <% if ( $type eq '' ) { %>
+            <font color="#ff0000">no type</font>
+          <% } elsif ( $type eq 'textarea' ) { %>
+            <textarea name="<%= $i->key. $n %>" rows=5><%= "\n". join("\n", $conf->config($i->key) ) %></textarea>
+          <% } elsif ( $type eq 'checkbox' ) { %>
+            <input name="<%= $i->key. $n %>" type="checkbox" value="1"<%= $conf->exists($i->key) ? ' CHECKED' : '' %>>
+          <% } elsif ( $type eq 'text' )  { %>
+            <input name="<%= $i->key. $n %>" type="<%= $type %>" value="<%= $conf->exists($i->key) ? $conf->config($i->key) : '' %>">
+          <% } elsif ( $type eq 'select' || $type eq 'selectmultiple' )  { %>
+            <select name="<%= $i->key. $n %>" <%= $type eq 'selectmultiple' ? 'MULTIPLE' : '' %>>
+              <% my %saw;
+                 foreach my $value ( "", @{$i->select_enum} ) {
+                    local($^W)=0; next if $saw{$value}++; %>
+                <option value="<%= $value %>"<%= $value eq $conf->config($i->key) || ( $type eq 'selectmultiple' && grep { $_ eq $value } $conf->config($i->key) ) ? ' SELECTED' : '' %>><%= $value %>
+              <% } %>
+              <% if ( $conf->exists($i->key) && $conf->config($i->key) && ! grep { $conf->config($i->key) eq $_ } @{$i->select_enum}) { %>
+                <option value=<%= $conf->config($i->key) %> SELECTED><%= $conf->config($i->key) %>
+              <% } %>
+            </select>
+          <% } elsif ( $type eq 'editlist' )  { %>
+            <script>
+              function doremove<%= $i->key. $n %>() {
+                fromObject = document.OneTrueForm.<%= $i->key. $n %>;
+                for (var i=fromObject.options.length-1;i>-1;i--) {
+                  if (fromObject.options[i].selected)
+                    deleteOption<%= $i->key. $n %>(fromObject,i);
+                }
+              }
+              function deleteOption<%= $i->key. $n %>(object,index) {
+                object.options[index] = null;
+              }
+              function selectall<%= $i->key. $n %>() {
+                fromObject = document.OneTrueForm.<%= $i->key. $n %>;
+                for (var i=fromObject.options.length-1;i>-1;i--) {
+                  fromObject.options[i].selected = true;
+                }
+              }
+              function doadd<%= $i->key. $n %>(object) {
+                var myvalue = "";
+                <% if ( defined($i->editlist_parts) ) { %>
+
+                  <% foreach my $pnum ( 0 .. scalar(@{$i->editlist_parts})-1 ) { %>
+
+                    if ( myvalue != "" ) { myvalue = myvalue + " "; }
+                    <% if ( $i->editlist_parts->[$pnum]{type} eq 'select' ) { %>
+                      myvalue = myvalue + object.add<%= $i->key. $n . "_$pnum" %>.options[object.add<%= $i->key. $n . "_$pnum" %>.selectedIndex].value;
+                      <!-- #RESET SELECT??  maybe not... -->
+                    <% } elsif ( $i->editlist_parts->[$pnum]{type} eq 'immutable' ) { %>
+                      myvalue = myvalue + object.add<%= $i->key. $n . "_$pnum" %>.value;
+                    <% } else { %>
+                      myvalue = myvalue + object.add<%= $i->key. $n . "_$pnum" %>.value;
+                      object.add<%= $i->key. $n. "_$pnum" %>.value = "";
+                    <% } %>
+
+
+                  <% } %>
+                <% } else { %>
+                  myvalue = object.add<%= $i->key. $n. "_1" %>.value;
+                <% } %>
+                var optionName = new Option(myvalue, myvalue);
+                var length = object.<%= $i->key. $n %>.length;
+                object.<%= $i->key. $n %>.options[length] = optionName;
+              }
+            </script>
+            <select multiple size=5 name="<%= $i->key. $n %>">
+            <option selected>----------------------------------------------------------------</option>
+            <% foreach my $line ( $conf->config($i->key) ) { %>
+              <option value="<%= $line %>"><%= $line %></option>
+            <% } %>
+            </select><br>
+            <input type="button" value="remove selected" onClick="doremove<%= $i->key. $n %>()">
+            <script>SafeAddOnLoad(doremove<%= $i->key. $n %>);
+                    SafeAddOnSubmit(selectall<%= $i->key. $n %>);</script>
+            <br>
+            <%= itable() %><tr>
+            <% if ( defined $i->editlist_parts ) { %>
+              <% my $pnum=0; foreach my $part ( @{$i->editlist_parts} ) { %>
+                <td>
+                <% if ( $part->{type} eq 'text' ) { %>
+                  <input type="text" name="add<%= $i->key. $n."_$pnum" %>">
+                <% } elsif ( $part->{type} eq 'immutable' ) { %>
+                  <%= $part->{value} %><input type="hidden" name="add<%= $i->key. $n. "_$pnum" %>" value="<%= $part->{value} %>">
+                <% } elsif ( $part->{type} eq 'select' ) { %>
+                  <select name="add<%= $i->key. $n. "_$pnum" %>">
+                  <% foreach my $key ( keys %{$part->{select_enum}} ) { %>
+                    <option value="<%= $key %>"><%= $part->{select_enum}{$key} %></option>
+                  <% } %>
+                  </select>
+                <% } else { %>
+                  <font color="#ff0000">unknown type <%= $part->type %></font>
+                <% } %>
+                </td>
+              <% $pnum++; } %>
+            <% } else { %>
+              <td><input type="text" name="add<%= $i->key. $n %>_0"></td>
+            <% } %>
+            <td><input type="button" value="add" onClick="doadd<%= $i->key. $n %>(this.form)"></td>
+            </tr></table>
+          <% } else { %>
+            <font color="#ff0000">unknown type <%= $type %></font>
+          <% } %>
+        <% $n++; } %>
+      </td>
+      <td><a name="<%= $i->key %>">
+        <b><%= $i->key %></b> - <%= $i->description %>
+      </a></td>
+    </tr>
+  <% } %>
+  </table><br>
+
+  You may need to restart Apache and/or freeside-queued for configuration
+  changes to take effect.<br>
+
+  <input type="submit" value="Apply changes"><br><br>
+
+<% } %>
+
+</form>
+
+</body></html>
diff --git a/httemplate/docs/admin.html b/httemplate/docs/admin.html
new file mode 100755 (executable)
index 0000000..50beafe
--- /dev/null
@@ -0,0 +1,81 @@
+<head>
+  <title>Administration</title>
+</head>
+<body>
+  <h1>Administration</h1>
+</body>
+<ul>
+  <li>Open up the root of the Freeside document tree in your web
+  browser.  For example, if you created the Freeside document tree in   
+  /home/httpd/html/freeside, and your web browser's DocumentRoot is
+  /home/httpd/html, open https://your_host/freeside/. Replace
+  "your_host" with the name or network address of your web server.
+  <li>Select <u>Configuration</u> from the main menu and update your configuration values.
+  <li>Next you must create a service definition.  An example of a service
+  definition would be a dial-up account or a domain.  First, it is
+  necessary to create a domain definition.  Click on <u>View/Edit service
+  definitions</u> and <u>Add a new service definition</u> with <i>Table</i>
+  <b>svc_domain</b> (and no modifiers).
+
+  <li>Now that you have created your first service, you must create a package
+  including this service which you can sell to customers.  Zero, one, or many
+  services are bundled into a package.  Click on <u>View/Edit package
+  definitions</u> and <u>Add a new package definition</u> which includes
+  quantity <b>1</b> of the svc_domain service you created above.
+
+  <li>After you create your first package, then you must define who is
+  able to sell that package by creating an agent type.  An example of
+  an agent type would be an internal sales representitive which sells
+  regular and promotional packages, as opposed to an external sales
+  representitive which would only sell regular packages of services.  Click on
+  <u>View/Edit agent types</u> and <u>Add a new agent type</u>.  Allow this
+  agent type to sell the package you created above.
+
+  <li>After creating a new agent type, you must create an agent.  Click on
+  <u>View/Edit agents</u> and <u>Add a new agent</u>.
+
+  <li>Set up at least one Advertising source.  Advertising sources will help
+  you keep track of how effective your advertising is, tracking where customers
+  heard of your service offerings.  You must create at least one advertising 
+  source.  If you do not wish to use the referral functionality, simply create
+  a single advertising source only.  Click on <u>View/Edit advertising
+  sources</u> and <u>Add a new advertising source</u>.
+
+  <li>Click on <u>New Customer</u> and create a new customer for your system
+  accounts with billing type <b>Complimentary</b>.  
+
+  <li>From the Customer View screen of the newly created customer, order the
+  package you defined above.
+
+  <li>From the Package View screen of the newly created package, choose
+  <u>(Provision)</u> to add the customer's service for this new package.
+
+  <li>Add your own domain.
+
+  <li>Go back to <u>View/Edit service definitions</u> on the main menu, and
+  <u>Add a new service definition</u> with <i>Table</i> <b>svc_acct</b>.
+  Select your domain in the <b>domsvc</b> Modifier.  Set <b>Fixed</b> to define
+  a service locked-in to this domain, or <b>Default</b> to define a service
+  which may select from among this domain and the customer's domains.
+
+  <li><table><tr>
+    <td> Create at least POP (Point of Presence) by selecting
+        <u>View/Edit POPs</u> from the main menu.</td>
+    <th align="left"> OR </th>
+    <td>If you are not doing dialup, set slipip to fixed and blank for all your
+        Service Definitions which have Table <b>svc_acct</b>.</td>
+  </tr></table>
+
+  <li>If you are using Freeside to keep track of sales taxes, define tax
+  information for your locales by clicking on the <u>View/Edit locales and tax
+  rates</b> on the main menu.
+
+  <li>If you would like Freeside to notify your customers when their credit
+  cards and other billing arrangements are about to expire, arrange for
+  <b>freeside-expiration-alerter</b> to be run daily by cron or similar
+  facility.  The message it sends can be configured from the
+  <u>Configuration</u> choice of the main menu as <u>alerter_template</u>.
+
+</ul>
+</body>
+</html>
diff --git a/httemplate/docs/billing.html b/httemplate/docs/billing.html
new file mode 100644 (file)
index 0000000..c78a87f
--- /dev/null
@@ -0,0 +1,54 @@
+<head>
+  <title>Billing</title>
+</head>
+<body>
+  <h1>Billing</h1>
+  <ul>
+    <li>You can bill individual customers by clicking on the <i>Bill now</i> link on the main customer view.
+    <li>The <a href="man/bin/freeside-daily.html"><b>freeside-daily</b></a> script should be run daily to bill customers and run invoice collection events.
+    <li>Real-time credit card processing: Install the <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> module for your processor.  Configure the <a href="../config/config-view.cgi#business-onlinepayment">business-onlinepayment</a> configuration option.  Disable the default <b>Batch card</b> <a href="../browse/part_bill_event.cgi">invoice event</a> and add one for Business::OnlinePayment.
+    <li>Optional: Credit card expiration alerts: Customize <a href="../config/config.cgi#alerter_template">alerter_template</a> configuration option and run <a href="man/bin/freeside-expiration-alerter.html">freeside-expiration-alerter</a> daily.
+    <li>Credit card decline alerts: Customize the <a href="../config/config.cgi#declinetemplate">declinetemplate</a> configuration option and set the <a href="../config/config.cgi#emaildecline">emaildecline</a> configuration option.
+    <li>Optional: Invoice template customization
+      <ul>
+        <li>See the <a href="http://search.cpan.org/doc/MJD/Text-Template-1.42/Template.pm">Text::Template</a> documentation for details on the substitution language.
+        <li>You <b>must</b> call the invoice_lines() function at least once - pass it a number of lines, and it returns a list of array references, each of two elements: a service description column, and a price column.  Alternatively, call invoice_lines() with no arguments, and pagination will be disabled - all invoice line items will print on one page, with no padding (recommended for email invoices).
+        <li>In addition, the following variables are available:
+          <ul>
+            <li>$invnum - invoice number
+            <li>$date - as a UNIX timestamp (see <a href="http://search.cpan.org/doc/GBARR/TimeDate-1.09/lib/Date/Format.pm">Date::Format</a> for conversion functions).
+            <li>$page - current page
+            <li>$total_pages - total pages
+            <li>@address - A six-element array containing the customer name, company, and address.
+<!--            <li>$overdue - true if this invoice is overdue -->
+          </ul>
+      </ul>
+    <li>Batch credit card processing
+      <ul>
+        <li>After <a href="man/bin/freeside-daily.html"><b>freeside-daily</b></a> is run, a credit card batch will be in the <a href="schema.html#cust_pay_batch">cust_pay_batch</a> table.  Export this table to your credit card batching.
+        <li>When your batch completes, erase the cust_pay_batch records in that batch and add any necessary paymants to the <a href="schema.html#cust_pay">cust_pay</a> table.  Example code to add payments is:
+        <pre>use FS::cust_pay;
+
+# loop over all records in batch
+
+my $payment=create FS::cust_pay (
+  'invnum' => $invnum,
+  'paid' => $paid,
+  '_date' => $_date,
+  'payby' => $payby,
+  'payinfo' => $payinfo,
+  'paybatch' => $paybatch,
+);
+
+my $error=$payment->insert;
+if ( $error ) {
+  #process error
+}
+
+# end loop
+</pre>
+All fields except paybatch are contained in the cust_pay_batch table.  You can use paybatch field to track particular batches and/or particular transactions within a batch.
+    </ul>
+<!--      <li>The <a href="man/bin/freeside-print-batch.html"><b>freeside-print-batch</b></a> script can print or email pending credit card batches for manual entry. -->
+  </ul>
+</body>
diff --git a/httemplate/docs/config.html b/httemplate/docs/config.html
new file mode 100644 (file)
index 0000000..9caf3bb
--- /dev/null
@@ -0,0 +1,36 @@
+<head>
+  <title>Configuration files</title>
+</head>
+<body>
+  <h1>Configuration files</h1>
+<font size="+1" color="#ff0000">Configuration is now done by the top-level Makefile and web interface.  The instructions below are no longer necessary.</font>
+<ul>
+  <li>Create the <b>/usr/local/etc/freeside</b> directory to hold your configuration.
+  <li>Setting up <a href="http://www.apache.org/docs/misc/FAQ.html#user-authentication">Apache user authetication</a> is mandatory.
+  <li>Create the <b>/usr/local/etc/freeside/mapsecrets</b> file, which maps Apache users to a secrets file which contains a DBI data source, username and password.  Every
+line in <b>/usr/local/etc/freeside/mapsecrets</b> should contain a username and
+filename, separated by whitespace.  Note that these are not local usernames -
+they are passed from Apache.  <a href="http://www.apache.org/docs/misc/FAQ.html#user-authentication">
+Apache user authetication</a> is mandatory.  For example, if you had the Apache users admin,
+john, and sam,  
+you mapsecrets file might look like:
+<pre>
+admin secretfile
+john secretfile
+sam secretfile
+</pre>
+  <li>Next, the filename(s) referenced in <b>/usr/local/etc/freeside/mapsecrets</b> file should be created in the <b>/usr/local/etc/freeside/</b> directory.  Each file contains three lines: <a href="http://search.cpan.org/doc/TIMB/DBI-1.20/DBI.pm">DBI data source</a> (for example,
+  <tt>DBI:mysql:freeside</tt> or <tt>DBI:Pg:host=localhost;dbname=freeside</tt>), database username, and database password.
+  These files should not be world readable.  See the <a href="http://search.cpan.org/doc/TIMB/DBI-1.20/DBI.pm">DBI manpage</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD">manpage for your DBD</a> for the exact syntax of a DBI data source.  In a normal installation such as the example above, a single file <b>/usr/local/etc/freeside/secretfile</b> would be created - for example:
+<pre>
+DBI:Pg:host=localhost;dbname=freeside
+dbusername
+dbpassword
+</pre>
+<li>Create the <b>/usr/local/etc/freeside/conf.<i>datasource</i></b> directory, for example, <b>/usr/local/etc/freeside/conf.DBI:Pg:host=localhost;dbname=freeside</b> (remember to backslash-escape the ; character when creating directories in the shell:
+<pre>mkdir&nbsp;/usr/local/etc/freeside/conf.DBI:Pg:host=localhost\;dbname=freeside
+</pre>
+<li>The rest of the configuration can be done with the web interface.  Select <u>Configuration</u> from the main menu and update your configuration values.
+</ul>
+</body>
+</html>
diff --git a/httemplate/docs/export.html b/httemplate/docs/export.html
new file mode 100755 (executable)
index 0000000..71e3acf
--- /dev/null
@@ -0,0 +1,55 @@
+<head>
+  <title>File exporting</title>
+</head>
+<body>
+  <h1>File exporting</h1>
+  <font size="+2">NOTE: This file is OUT OF DATE with the landing of the new export code and is only here for reference.  DO NOT follow these instructions.  Instead use the new exports in the web interface.</font>
+  <ul>
+    <li>bin/svc_acct.export will create UNIX <b>passwd</b>, <b>shadow</b> and <b>master.passwd</b> files, ERPCD <b>acp_passwd</b> and <b>acp_dialup</b> files and a RADIUS <b>users</b> file in the <b>/usr/local/etc/freeside/export.<i>datasrc</i></b> directory.  Some RADIUS servers (such as <a href="http://www.open.com.au/radiator/">Radiator</a>, <a href="ftp://ftp.cheapnet.net/pub/icradius/">ICRADIUS</a> and <a href="http://www.freeradius.org/">FreeRADIUS</a>) will authenticate directly out of an SQL database.  In these cases,
+it is reccommended that you replicate (<a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">Replication in MySQL</a>) the data to an external RADIUS machine or point icradius_secrets to the external machine rather than running the RADIUS server on your Freeside machine.  Using the appropriate <a href="../config/config-view.cgi">configuration settings</a>, you can export these files to your remote machines unattended:
+      <ul>
+        <li>shellmachines - <b>passwd</b> and <b>shadow</b> are copied to the remote machine as <b>/etc/passwd.new</b> and <b>/etc/shadow.new</b> and then moved to <b>/etc/passwd</b> and <b>/etc/shadow</b> if no errors occur.
+        <li>bsdshellmachines - <b>passwd</b> and <b>master.passwd</b> are copied to the remote machine as <b>/etc/passwd.new</b> and <b>/etc/master.passwd.new</b> and moved to <b>/etc/passwd</b> and <b>/etc/master.passwd</b> if no errors occur.
+        <li>nismachines - <b>passwd</b> and <b>shadow</b> are copied to the <b>/etc/global</b> directory on the remote machine.  If no errors occur, the command <b>( cd /var/yp; make; )</b> is executed on the remote machine.
+        <li>erpcdmachines - <b>acp_passwd</b> and <b>acp_dialup</b> are copied to the <b>/usr/annex</b> directory on the remote machine.  If no errors occur, the command <b>( kill -USR1 `cat /usr/annex/erpcd.pid` )</b> is executed on the remote machine. 
+        <li>radiusmachines - <b>users</b> is copied to the <b>/etc/raddb</b> directory on the remote machine.  If no errors occur, the command <b>( builddbm )</b> is executed on the remote machine.
+        <li>icradiusmachines - Turn this option on to enable radcheck table population - by default in the Freeside database, or in the database specified by the <a href="http://rootwood.haze.st/aspside/config/config-view.cgi#icradius_secrets">icradius_secrets</a> config option (the radcheck table needs to be created manually).  You do not need to use MySQL for your Freeside database to export to an ICRADIUS/FreeRADIUS MySQL database with this option.  <blockquote><b>ADDITIONAL DEPRECATED FUNCTIONALITY</b> (instead use <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">MySQL replication</a> or point icradius_secrets to the external database) - your <a href="ftp://ftp.cheapnet.net/pub/icradius">ICRADIUS</a> machines or <a href="http://www.freeradius.org/">FreeRADIUS</a> (with MySQL authentication) machines, one per line.  Machines listed in this file will have the radcheck table exported to them.  Each line should contain four items, separted by whitespace: machine name, MySQL database name, MySQL username, and MySQL password.  For example: <CODE>"radius.isp.tld&nbsp;radius_db&nbsp;radius_user&nbsp;passw0rd"</CODE></blockquote>
+      </ul>
+    <li>svc_acct.pm - If a shellmachine is defined, users can be created, modified and deleted remotely; see below.
+      <ul>
+        <li>Account creation - If the <b>username</b>, <b>uid</b> and <b>dir</b> fields are defined for a new user, the command(s) specified in the <a href="../config/config-view.cgi#shellmachine-useradd">shellmachine-useradd</a> configuration file are executed on shellmachine via ssh.  If this file does not exist, <code>useradd -d $dir -m -s $shell -u $uid $username</code> is the default.  If the file exists but is empty, <code>cp -pr /etc/skel $dir; chown -R $uid.$gid $dir</code> is the default instead.  Otherwise the contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$username</code>, <code>$uid</code>, <code>$gid</code>, <code>$dir</code>, and <code>$shell</code>.
+        <li>Account deletion - The command(s) specified in the <a href="../config/config-view.cgi#shellmachine-userdel">shellmachine-userdel</a> configuration file are executed on shellmachine via ssh.  If this file does not exist, <code>userdel $username</code> is the default.  If the file exists but is empty, <code>rm -rf $dir</code> is the default instead.  Otherwise the contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$username</code> and <code>$dir</code>.
+        <li>Account modification - If a user's home directory changes, the command(s) specified in the <a href="../config/config-view.cgi#shellmachine-usermod">shellmachine-usermod</a> configuration file are execute on shellmachine via ssh.  If this file does not exist or is empty, <code>[ -d $old_dir ] &amp;&amp; mv $old_dir $new_dir || ( chmod u+t $old_dir; mkdir $new_dir; cd $old_dir; find . -depth -print | cpio -pdm $new_dir; chmod u-t $new_dir; chown -R $uid.$gid $new_dir; rm -rf $old_dir )</code> is the default.  Otherwise the contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$old_dir</code>, <code>$new_dir</code>, <code>$uid</code> and <code>$gid</code>.
+      </ul>
+    <li>svc_acct.pm - <a href="http://asg.web.cmu.edu/cyrus/imapd/">Cyrus IMAP Server</a> integration, enabled by the <a href="../config/config-view.cgi#cyrus">cyrus configuration file</a>
+      <ul>
+        <li>Account creation - (Cyrus::IMAP::Admin should be installed locally)
+        <li>Account deletion - (Cyrus::IMAP::Admin should be installed locally)
+        <li>Account modification - (not yet implemented)
+      </ul>
+    <li>bin/svc_acct_sm.export will create <a href="http://www.qmail.org">Qmail</a> <b>rcpthosts</b>, <b>recipientmap</b> and <b>virtualdomains</b> files and <a href="http://www.sendmail.org">Sendmail</a> <b>virtusertable</b> and <b>sendmail.cw</b> files in the <b>/usr/local/etc/freeside/export.<i>datasrc</i></b> directory.  Using the appropriate <a href="../config/config-view.cgi">configuration files</a>, you can export these files to your remote machines unattemded:
+      <ul>
+        <li>qmailmachines - <b>recipientmap</b>, <b>virtualdomains</b> and <b>rcpthosts</b> are copied to the <b>/var/qmail/control</b> directory on the remote machine.  Note: If you <a href="legacy.html#svc_acct_sm">imported</a> qmail configuration files, run the generated <b>/usr/local/etc/freeside/export.<i>datasrc</i>/virtualdomains.FIX</b> on a machine with your user home directories before exporting qmail configuration files.
+        <li>shellmachine - The command <b>[ -e <i>homedir</i>/.qmail-default ] || { touch <i>homedir</i>/.qmail-default; chown <i>uid</i>.<i>gid</i> <i>homedir</i>/.qmail-default; }</b> will be run on this machine for users in the virtualdomains file.
+        <li>sendmailmachines - <b>sendmail.cw</b> and <b>virtusertable</b> are copied to the remote machine as <b>/etc/sendmail.cw.new</b> and <b>/etc/virtusertable.new</b>.  If no errors occur, they are moved to <b>/etc/sendmail.cw</b> and <b>/etc/virtusertable</b> and the command specified in the <a href="../config/config-view.cgi#sendmailrestart">sendmailrestart</a> configuration file is executed.  (The path can be changed from the default <b>/etc</b> with the <a href="../config/config-view.cgi#sendmailconfigpath">sendmailconfigpath</a> configuration file.)
+      </ul>
+    <li>svc_domain.pm - If the qmailmachines configuration file exists and a shellmachine is defined, user <b>.qmail-</b> files can be updated for catchall mailboxes.
+      <ul>
+        <li>The command <pre>[ -e <i>homedir</i>/.qmail-<i>domain</i>-default ] || {
+    touch <i>homedir</i>/.qmail-<i>domain</i>-default;
+    chown <i>uid</i>.<i>gid</i> <i>homedir</i>/.qmail-<i>domain</i>-default;
+}</pre> is run.
+      </ul>
+    <li>svc_forward.pm - Not yet documented; see manpage.
+    <li>svc_www.pm - Not yet documented; see manpage.
+  </ul>
+  <br><a name=ssh>Unattended remote login</a> - Freeside can login to remote machines unattended using SSH.  This can pose a security risk if not configured correctly, and will allow an intruder who breaks into your freeside machine full access to your remote machines.  <b>Do not use this feature unless you understand what you are doing!</b>
+    <ul>
+      <li>As the freeside user (on your freeside machine), generate an authentication key using <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>.  Since this is for unattended operation, use a blank passphrase.
+      <li>Append the newly-created <code>identity.pub</code> file to <code>~root/.ssh/authorized_keys</code> on the remote machine(s).
+      <li>Some new SSH v2 implementation accept v2 style keys only.  Use the <code>-t</code> option to <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>, and append the created <code>id_dsa.pub</code> or <code>id_rsa.pub</code> to <code>~root/.ssh/authorized_keys2</code> on the remote machine(s).
+      <li>You may need to set <code>PermitRootLogin without-password</code> (meaning with keys only) in your <code>sshd_config</code> file on the remote machine(s).
+    </ul>
+
+</body>
+
diff --git a/httemplate/docs/index.html b/httemplate/docs/index.html
new file mode 100644 (file)
index 0000000..b57b06f
--- /dev/null
@@ -0,0 +1,29 @@
+<head>
+  <title>Documentation</title>
+</head>
+<body bgcolor="#ffffff">
+  <h1>Documentation</h1>
+<img src="overview.png">
+<ul>
+  <li><a href="install.html">New Installation</a>
+  <li><a href="upgrade7.html">Upgrading from 1.3.0 to 1.3.1</a>
+  <li><a href="upgrade8.html">Upgrading from 1.3.1 to 1.4.0</a>
+  <li><a href="upgrade9.html">Upgrading from 1.4.0 to 1.4.1</a>
+  <li><a href="upgrade10.html">Upgrading from 1.4.1 (or 1.4.2?) to 1.5.0</a>
+<!--
+  <li><a href="config.html">Configuration files</a>
+!-->
+  <li><a href="admin.html">Administration</a>
+<!--
+  <li><a href="../index.html#admin">Administration</a>
+!-->
+  <li><a href="legacy.html">Importing legacy data</a>
+  <li><a href="export.html">File exporting and remote setup</a>
+  <li><a href="passwd.html">fs_passwd</a>
+  <li><a href="signup.html">Signup server</a>
+  <li><a href="session.html">Session monitor</a>
+  <li><a href="billing.html">Billing</a>
+  <li><a href="schema.html">Schema reference</a>
+  <li><a href="man/FS.html">Perl API</a>
+</ul>
+</body>
diff --git a/httemplate/docs/install.html b/httemplate/docs/install.html
new file mode 100644 (file)
index 0000000..54614cc
--- /dev/null
@@ -0,0 +1,206 @@
+<head>
+  <title>Installation</title>
+</head>
+<body>
+<h1>Installation</h1>
+Before installing, you need:
+<ul>
+  <li><a href="http://www.perl.com/">Perl</a>  Don't enable experimental features like threads or the PerlIO abstraction layer.
+  <li><a href="http://www.apache.org">Apache</a> (<a href="http://www.modssl.org/">mod_ssl</a> or <a href="http://www.apache-ssl.org">Apache-SSL</a> highly recommended)
+  <li><a href="http://perl.apache.org/">mod_perl</a> (if compiling your own mod_perl, make sure you set the <a href="http://perl.apache.org/guide/install.html#EVERYTHING">EVERYTHING</a>=1 compile-time option)
+  <li><a href="http://www.openssh.com/">SSH</a> (<a href="http://www.openssh.com//">OpenSSH</a> is recommended.  SSH Communications Security <a href="http://www.ssh.com/products/ssh/download.cfm">commercial SSH version 3</a> has been reported incompatible with Freeside.)
+  <li><a href="http://rsync.samba.org/">rsync</a>
+  <li>A <b>transactional</b> database engine <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">supported</a> by Perl's <a href="http://dbi.perl.org">DBI</a>.
+    <ul>
+      <li><a href="http://www.postgresql.org/">PostgreSQL</a> is recommended (v7or later).
+      <li><a href="http://www.mysql.com/">MySQL</a> <b>MINIMUM VERSION 4.1</b> is untested but may work.   Versions before 4.1 do not support standard SQL subqueries and are <b>NOT SUPPORTED</b>.  If you are a developer who wishes to contribute MySQL 3.x/4.0 support, see <a href="http://pouncequick.420.am/rt/Ticket/Display.html?id=438">ticket #438</a> in the bug-tracking system and ask on the -devel mailing list.
+<!--       <li>MySQL has been reported to work. -->
+         <b>MySQL's default <a href="http://www.mysql.com/doc/M/y/MyISAM.html">MyISAM</a> and <a href="http://www.mysql.com/doc/I/S/ISAM.html">ISAM</a> table types are not supported</b>.  If you want to use MySQL, you <b>must</b> use one of the new <a href="http://www.mysql.com/doc/T/a/Table_types.html">transaction-safe table types</a> such as <a href="http://www.mysql.com/doc/B/D/BDB.html">BDB</a> or <a href="http://www.mysql.com/doc/I/n/InnoDB.html">InnoDB</a>, and set it as the default table type using the <code>--default-table-type=BDB</code> or <code>--default-table-type=InnoDB</code> <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Command-line_options">mysqld command-line option</a> or by setting <code>default-table-type=BDB</code> or <code>default-table-type=InnoDB</code> in the <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Option_files">my.cnf option file</a>.
+    </ul>
+  <li>Perl modules (<a href="http://theoryx5.uwinnipeg.ca/CPAN/perl/CPAN.html">CPAN</a> will query, download and build perl modules automatically)
+    <ul>
+<!--      <li><a href="http://search.cpan.org/search?dist=Array-PrintCols">Array-PrintCols</a>
+      <li><a href="http://search.cpan.org/search?dist=Term-Query">Term-Query</a> (make test broken; install manually) -->
+      <li><a href="http://search.cpan.org/search?dist=MIME-Base64">MIME-Base64</a>
+      <li><a href="http://search.cpan.org/search?dist=Digest-MD5">Digest-MD5</a>
+<!--      <li><a href="http://search.cpan.org/search?dist=MD5">MD5</a> -->
+      <li><a href="http://search.cpan.org/search?dist=URI">URI</a>
+      <li><a href="http://search.cpan.org/search?dist=HTML-Tagset">HTML-Tagset</a>
+      <li><a href="http://search.cpan.org/search?dist=HTML-Parser">HTML-Parser</a>
+      <li><a href="http://search.cpan.org/search?dist=libnet">libnet</a>
+      <li><a href="http://search.cpan.org/search?dist=Locale-Codes">Locale-Codes</a>
+      <li><a href="http://search.cpan.org/search?dist=Net-Whois">Net-Whois</a>
+      <li><a href="http://search.cpan.org/search?dist=libwww-perl">libwww-perl</a>
+      <li><a href="http://search.cpan.org/search?dist=Business-CreditCard">Business-CreditCard</a>
+<!--      <li><a href="http://search.cpan.org/search?dist=Data-ShowTable">Data-ShowTable</a> -->
+      <li><a href="http://search.cpan.org/search?dist=MailTools">MailTools</a>
+      <li><a href="http://search.cpan.org/search?dist=TimeDate">TimeDate</a>
+      <li><a href="http://search.cpan.org/search?dist=DateManip">DateManip</a>
+      <li><a href="http://search.cpan.org/search?dist=File-CounterFile">File-CounterFile</a>
+      <li><a href="http://search.cpan.org/search?dist=FreezeThaw">FreezeThaw</a>
+      <li><a href="http://search.cpan.org/search?dist=String-Approx">String-Approx</a>
+      <li><a href="http://search.cpan.org/search?dist=Text-Template">Text-Template</a>
+      <li><a href="http://search.cpan.org/search?dist=DBI">DBI</a>
+      <li><a href="http://search.cpan.org/search?mode=module&query=DBD">DBD for your database engine</a> (<a href="http://search.cpan.org/search?dist=DBD-Pg">DBD::Pg</a> for PostgreSQL, <a href="http://search.cpan.org/search?dist=DBD-mysql">DBD::mysql</a> for MySQL)
+      <li><a href="http://search.cpan.org/search?dist=DBIx-DataSource">DBIx-DataSource</a>
+      <li><a href="http://search.cpan.org/search?dist=DBIx-DBSchema">DBIx-DBSchema</a>
+      <li><a href="http://search.cpan.org/search?dist=Net-SSH">Net-SSH</a>
+      <li><a href="http://search.cpan.org/search?dist=String-ShellQuote">String-ShellQuote</a>
+      <li><a href="http://search.cpan.org/search?dist=Net-SCP">Net-SCP</a>
+      <li><a href="http://www.apache-asp.org/">Apache::ASP</a> or <a href="http://www.masonhq.com/">HTML::Mason</a>
+      <li><a href="http://search.cpan.org/search?dist=Tie-IxHash">Tie-IxHash</a>
+      <li><a href="http://search.cpan.org/search?dist=Time-Duration">Time-Duration</a>
+      <li><a href="http://search.cpan.org/search?dist=HTML-Widgets-SelectLayers">HTML-Widgets-SelectLayers</a>
+      <li><a href="http://search.cpan.org/search?dist=Storable">Storable</a>
+<!-- MyAccounts, maybe only for dev     <li><a href="http://search.cpan.org/search?dist=Cache-Cache">Cache::Cache</a> -->
+      <li><a href="http://search.cpan.org/search?dist=NetAddr-IP">NetAddr-IP</a>
+      <li><a href="http://search.cpan.org/search?dist=Chart">Chart</a>
+      <li><a href="http://search.cpan.org/search?dist=ApacheDBI">Apache::DBI</a> <i>(optional but recommended for better webinterface performance)</i>
+    </ul>
+</ul>
+Install the Freeside distribution:
+<ul>
+  <li>Add the user and group `freeside' to your system.
+  <li>Allow the freeside user full access to the freeside database.
+    <ul>
+      <li> with <a href="http://www.postgresql.org/users-lounge/docs/7.1/postgres/user-manag.html#DATABASE-USERS">PostgreSQL</a>:
+        <pre>
+$ su postgres (pgsql on some distributions)
+$ createuser -P freeside
+Enter password for user "freeside": 
+Enter it again: 
+Shall the new user be allowed to create databases? (y/n) y
+Shall the new user be allowed to create more new users? (y/n) n
+CREATE USER</pre>
+      <li> with <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#User_Account_Management">MySQL</a>:
+        <pre>
+$ mysqladmin -u root password '<i>set_a_root_database_password</i>'
+$ mysql -u root -p
+mysql> GRANT SELECT,INSERT,UPDATE,DELETE,INDEX,ALTER,CREATE,DROP on freeside.* TO freeside@localhost IDENTIFIED BY '<i>set_a_freeside_database_password</i>';</pre>
+    </ul>
+<!--  <li>Unpack the tarball: <pre>gunzip -c fs-x.y.z.tar.gz | tar xvf -</pre>-->
+  <li>Edit the top-level Makefile:
+    <ul>
+      <li>Set <tt>DATASOURCE</tt> to your <a href="http://search.cpan.org/doc/TIMB/DBI-1.28/DBI.pm">DBI data source</a>, for example, <tt>DBI:Pg:dbname=freeside</tt> for PostgresSQL or <tt>DBI:mysql:freeside</tt> for MySQL.  See the <a href="http://search.cpan.org/doc/TIMB/DBI-1.28/DBI.pm">DBI manpage</a> and the <a href="http://search.cpan.org/search?mode=module&query=DBD%3A%3A">manpage for your DBD</a> for the exact syntax of your DBI data source.
+      <li>Set <tt>DB_PASSWORD</tt> to the freeside database user's password.
+    </ul>
+  <li>Add the freeside database to your database engine:
+    <pre>
+$ su
+# make create-database</pre>
+    (or manually, with Postgres:)
+    <pre>
+$ su freeside
+$ createdb freeside</pre>
+    (with MySQL:)
+    <pre>
+$ mysqladmin -u freeside -p create freeside </pre>
+  <li>Build and install the Perl modules:
+    <pre>
+$ make perl-modules
+$ su
+# make install-perl-modules</pre>
+    <li>Create the necessary configuration files:<pre>
+$ su
+# make create-config
+</pre>
+    <li>Run a <b>separate</b> iteration of Apache[-SSL] with mod_perl enabled <b>as the freeside user</b>.
+</ul>
+<table>
+  <tr>
+    <th>Apache::ASP</th><th>Mason</th>
+  </tr>
+  <tr>
+    <td><ul>
+      <li>Run <tt>make aspdocs</tt>
+      <li>Copy <tt>aspdocs/</tt> to your web server's document space:
+<font size="-1"><pre>
+cp&nbsp;aspdocs&nbsp;/usr/local/apache/htdocs/freeside-asp
+</pre></font>
+      <li>Create a <a href="http://www.apache-asp.org/config.html#Global">Global</a> directory, such as <tt>/usr/local/etc/freeside/asp-global/</tt>:
+<font size="-1"><pre>
+mkdir&nbsp;/usr/local/etc/freeside/asp-global/
+chown&nbsp;freeside&nbsp;/usr/local/etc/freeside/asp-global/
+</pre></font>
+      <li>Copy <tt>htetc/global.asa</tt> to the Global directory:
+<font size="-1"><pre>
+cp&nbsp;htetc/global.asa&nbsp;/usr/local/etc/freeside/asp-global/global.asa
+</pre></font>
+      <li>Configure Apache for the Global directory and to execute .cgi files using Apache::ASP.  For example:
+<font size="-1"><pre>
+PerlModule Apache::ASP
+&lt;Directory&nbsp;/usr/local/apache/htdocs/freeside-asp&gt;
+&lt;Files ~ (\.cgi)&gt;
+AddHandler perl-script .cgi
+PerlHandler Apache::ASP
+&lt;/Files&gt;
+&lt;Perl&gt;
+$MLDBM::RemoveTaint = 1;
+&lt;/Perl&gt;
+PerlSetVar&nbsp;Global&nbsp;/usr/local/etc/freeside/asp-global/
+PerlSetVar Debug 2
+&lt;/Directory&gt;
+</pre></font>
+    </ul></td>
+    <td><ul>
+      <li>Run <tt>make masondocs</tt>
+      <li>Copy <tt>masondocs/</tt> to your web server's document space. (For example: <tt>/usr/local/apache/htdocs/freeside-mason</tt>)
+      <li>Copy <tt>htetc/handler.pl</tt> to <tt>/usr/local/etc/freeside</tt> (use htetc/handler.pl-1.0x for Mason versions before 1.10).
+      <li>Edit <tt>handler.pl</tt> and:
+      <ul>
+        <li> set an appropriate <tt>comp_root</tt>, such as <tt>/usr/local/apache/htdocs/freeside-mason</tt>
+        <li> set an appropriate <tt>data_dir</tt>, such as <tt>/usr/local/etc/freeside/masondata</tt>
+      </ul>
+
+      <li>Configure Apache to use the <tt>handler.pl</tt> file and to execute .cgi files using HTML::Mason.  For example:
+<font size="-1"><pre>
+PerlModule HTML::Mason
+&lt;Directory&nbsp;/usr/local/apache/htdocs/freeside-mason&gt;
+&lt;Files ~ (\.cgi)&gt;
+AddHandler perl-script .cgi
+PerlHandler HTML::Mason
+&lt;/Files&gt;
+&lt;Perl&gt;
+require&nbsp;"/usr/local/etc/freeside/handler.pl";
+&lt;/Perl&gt;
+&lt;/Directory&gt;
+</pre></font>
+    </ul></td>
+  </tr>
+</table>
+<ul>
+<li>Restrict access to this web interface - see the <a href="http://httpd.apache.org/docs/misc/FAQ.html#user-authentication">Apache documentation on user authentication</a>.    For example, to configure user authentication with <a href="http://httpd.apache.org/docs/mod/mod_auth.html">mod_auth</a> (flat files):
+<pre>
+&lt;Directory /usr/local/apache/htdocs/freeside-asp&gt;
+AuthName Freeside
+AuthType Basic
+AuthUserFile /usr/local/etc/freeside/htpasswd
+require valid-user
+&lt;/Directory&gt;
+</pre>
+  <li>Create one or more Freeside users (your internal sales/tech folks, not customer accounts).  These users are setup using using Apache authentication, not UNIX user accounts.  For example, using <a href="http://httpd.apache.org/docs/mod/mod_auth.html">mod_auth</a> (flat files):
+    <ul>
+      <li>First user:<font size="-1">
+<pre>$ su
+$ <a href="man/bin/freeside-adduser.html">freeside-adduser</a> -c -h /usr/local/etc/freeside/htpasswd <i>username</i></pre></font>
+      <li>Additional users:<font size="-1">
+<pre>$ su
+$ <a href="man/bin/freeside-adduser.html">freeside-adduser</a> -h /usr/local/etc/freeside/htpasswd <i>username</i></pre></font>
+    </ul>
+  <i>(using other auth types, add each user to your <a href="http://httpd.apache.org/docs/misc/FAQ.html#user-authentication">Apache authentication</a> and then run: <tt>freeside-adduser <b>username</b></tt></i>
+  <li>As the freeside UNIX user, run <tt>freeside-setup <b>username</b></tt> to create the database tables, passing the username of a Freeside user you created above:
+<pre>
+$ su freeside
+$ freeside-setup <b>username</b>
+</pre>
+  Alternately, use the -s option to enable shipping addresses: <tt>freeside-setup -s <b>username</b></tt>
+  <li>As the freeside UNIX user, run <tt>bin/populate-msgcat <b>username</b></tt> (in the untar'ed freeside directory) to populate the message catalog, passing the username of a Freeside user you created above:
+<pre>
+$ su freeside
+$ cd <b>/path/to/freeside/</b>
+$ bin/populate-msgcat <b>username</b>
+</pre>
+  <li><tt>freeside-queued</tt> was installed with the Perl modules.  Start it now and ensure that is run upon system startup (Do this manually, or edit the top-level Makefile, replacing INIT_FILE with the appropriate location on your systemand QUEUED_USER with the username of a Freeside user you created above, and run <tt>make install-init</tt>)
+  <li>Now proceed to the initial <a href="admin.html">administration</a> of your installation.
+</ul>
+</body>
diff --git a/httemplate/docs/legacy.html b/httemplate/docs/legacy.html
new file mode 100755 (executable)
index 0000000..6787809
--- /dev/null
@@ -0,0 +1,39 @@
+<head>
+  <title>Importing legacy data</title>
+</head>
+<body>
+  <h1>Importing legacy data</h1>
+<font size="+2">In most cases, legacy data import all cases will require writing custom code to deal with your particular legacy data.  The example scripts here will not work "out-of-the-box".  Importing your legacy data will most probably involve some hacking on the example scripts noted below.  Contributions to the import process are welcome.</font>
+<br><br><i>Some import scripts may require installation of the <a href="http://search.cpan.org/search?dist=Array-PrintCols">Array-PrintCols</a> and <a href="http://search.cpan.org/search?dist=Term-Query">Term-Query</a> (make test broken; install manually) modules.</i><br>
+<ul>
+  <li><a name="bind">bin/bind.import</a> - Import domain information from BIND named
+  <li><a name="passwd">bin/passwd.import</a> - Just import `passwd' and `shadow' or `master.passwd', no RADIUS import.
+  <li><a name="svc_acct">bin/svc_acct.import</a> - Import `passwd', ( `shadow' or `master.passwd' ) and RADIUS `users'.  Before running bin/svc_acct.import, you need <a href="../browse/part_svc.cgi">services</a> (with table svc_acct) as follows:
+    <ul>
+      <li>Most accounts probably have entries in passwd and users (with Port-Limit nonexistant or 1)
+      <li>Some accounts have entries in passwd and users, but with Port-Limit 2 (or more)
+      <li>Some accounts might have entries in users only (Port-Limit 1)
+      <li>Some accounts might have entries in users only (Port-Limit >= 2)
+      <li>POP mail accounts have entries in passwd only, and have a particular shell.
+      <li>Everything else in passwd is a shell account.
+    </ul>
+<!--  <li><a name="svc_acct_sm">bin/svc_acct_sm.import</a> - Import qmail ( `virtualdomains' and `rcpthosts' ), or sendmail ( `virtusertable' and `sendmail.cw' ) files.  Before running bin/svc_acct_sm.import, you need <a href="../browse/part_svc.cgi">services</a> as follows:
+    <ul>
+      <li>Domain (table svc_acct)
+      <li>Mail alias (table svc_acct_sm)
+    </ul>
+-->
+  <li><a name="cust_main">Importing customer data</a>
+    <ul>
+      <li>Manually
+        <ul>
+          <li>Add a <a href="../edit/cust_main.cgi">new customer</a>
+          <li>Add one or more packages for this customer
+          <li>Enter a package by clicking on the package number
+          <li>Pick the `Link to existing' option
+        </ul>
+      <li>Batch - You will need to write a script to import your particular legacy data.  You can use eg/TEMPLATE_cust_main.import as a starting point.
+    </ul>
+</ul>
+</body>
+
diff --git a/httemplate/docs/man/FS/part_export/.cvs_is_on_crack b/httemplate/docs/man/FS/part_export/.cvs_is_on_crack
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/httemplate/docs/overview.dia b/httemplate/docs/overview.dia
new file mode 100644 (file)
index 0000000..a0e34c3
Binary files /dev/null and b/httemplate/docs/overview.dia differ
diff --git a/httemplate/docs/overview.png b/httemplate/docs/overview.png
new file mode 100644 (file)
index 0000000..bf2dbc2
Binary files /dev/null and b/httemplate/docs/overview.png differ
diff --git a/httemplate/docs/passwd.html b/httemplate/docs/passwd.html
new file mode 100755 (executable)
index 0000000..fc1dde9
--- /dev/null
@@ -0,0 +1,23 @@
+<head>
+  <title>fs_passwd</title>
+</head>
+<body>
+  <h1>fs_passwd</h1>
+You may use fs_passwd/fs_passwd as a "passwd", "chfn" and "chsh" replacement on your shell machine(s) to cause password, gecos and shell changes to update your freeside machine.  You can also use the fs_passwd/fs_passwd.html and fs_passwd/fs_passwd.cgi to run a public password change CGI on a public web server.  This can pose a security risk if not configured correctly.  <b>Do not use this feature unless you understand what you are doing!</b>
+<br><br>Currently it is assumed that the the crypt(3) function in the C library is the same on the Freeside machine as on the target machine.
+<ul>
+  <li>Create a freeside account on the shell or web machine(s).
+  <li>Setup SSH keys:
+    <ul>
+      <li>As the freeside user (on your freeside machine), generate an authentication key using <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>.  Since this is for unattended operation, use a blank passphrase.
+      <li>Append the newly-created <code>identity.pub</code> file to <code>~freeside
+/.ssh/authorized_keys</code> on the shell or web machine(s).
+      <li>Some new SSH v2 implementation accept v2 style keys only.  Use the <code>-t</code> option to <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>, and append the created <code>id_dsa.pub</code> or <code>id_rsa.pub</code> to <code>~freeside/.ssh/authorized_keys2</code> on the remote machine(s).
+    </ul>
+  <li>Copy fs_passwd/fs_passwdd to /usr/local/sbin on the shell or web machine(s).  (chown freeside, chmod 500)
+  <li>Create /usr/local/freeside on the shell or web machine(s). (chown freeside, chmod 700)
+  <li>Run an iteration of "fs_passwd/fs_passwd_server <i>user</i> shell.machine" as the freeside user for each shell or web machine (this is a daemon process).  <i>user</i> refers to a freeside user added by <a href="man/bin/freeside-adduser.html">freeside-adduser</a>.
+  <li>Copy fs_passwd/fs_passwd to /usr/local/bin on the shell machine(s).  (chown freeside, chmod 4755).  You may link it to passwd, chfn and chsh as well.
+  <li>Copy fs_passwd/fs_passwd.cgi to the cgi-bin directory on your web machine(s).  Use <a href="http://www.apache.org/docs/suexec.html">suEXEC</a> or <a href="http://www.perldoc.com/perl5.6.1/pod/perlsec.html">suidperl</a> to run fs_passwd.cgi as the freeside user.
+</ul>
+</body>
diff --git a/httemplate/docs/schema.dia b/httemplate/docs/schema.dia
new file mode 100644 (file)
index 0000000..7465615
Binary files /dev/null and b/httemplate/docs/schema.dia differ
diff --git a/httemplate/docs/schema.html b/httemplate/docs/schema.html
new file mode 100644 (file)
index 0000000..a59755e
--- /dev/null
@@ -0,0 +1,427 @@
+<head>
+  <title>Schema reference</title>
+</head>
+<body>
+  <h1>Schema reference</h1>
+  Schema diagram: <a href="schema.png">as a giant .png</a> or <a href="schema.dia">dia source</a> (<a href="http://www.lysator.liu.se/~alla/dia/">dia homepage</a>).
+  <ul>
+    <li><a name="agent" href="man/FS/agent.html">agent</a> - Agents are resellers of your service.  Agents may be limited to a subset of your full offerings (via their agent type).
+      <ul>
+        <li>agentnum - primary key
+        <li>agent - name of this agent
+        <li>typenum - <a href="#agent_type">agent type</a>
+        <li>prog - (unimplemented)
+        <li>freq - (unimplemented)
+      </ul>
+    <li><a name="agent_type" href="man/FS/agent_type.html">agent_type</a> - Agent types define groups of packages that you can then assign to particular agents.
+      <ul>
+        <li>typenum - primary key
+        <li>atype - name of this agent type
+      </ul>
+    <li><a name="cust_bill" href="man/FS/cust_bill.html">cust_bill</a> - Invoices.  Declarations that a customer owes you money.  The specific charges are itemized in <a href="#cust_bill_pkg">cust_bill_pkg</a>.
+      <ul>
+        <li>invnum - primary key
+        <li>custnum - <a href="#cust_main">customer</a>
+        <li>_date
+        <li>charged - amount of this invoice
+        <li>printed - how many times this invoice has been printed automatically
+        <li>closed - books closed flag, empty or `Y'
+      </ul>
+    <li><a name="cust_bill_event" href="man/FS/cust_bill_event.html">cust_bill_event</a> - Invoice event history
+      <ul>
+        <li>eventnum - primary key
+        <li>invnum - <a href="#cust_bill">invoice</a>
+        <li>eventpart - <a href="#part_bill_event">event definition</a>
+        <li>_date
+        <li>status
+        <li>statustext
+      </ul>
+    <li><a name="part_bill_event" href="man/FS/part_bill_event.html">part_bill_event</a> - Invoice event definitions
+      <ul>
+        <li>eventpart - primary key
+        <li>payby - CARD, DCRD, CHEK, DCHK, LECB, BILL, or COMP
+        <li>event - event name
+        <li>eventcode - event action
+        <li>seconds - how long after the invoice date (<a href="#cust_bill">cust_bill</a>._date) events of this type are triggered
+        <li>weight - ordering for events with identical seconds
+        <li>plan - eventcode plan
+        <li>plandata - additional plan data
+        <li>disabled - Disabled flag, empty or `Y'
+        <li>taxclass - Texas tax class flag, empty or "none", "access", or "hosting"
+      </ul>
+    <li><a name="cust_bill_pkg" href="man/FS/cust_bill_pkg.html">cust_bill_pkg</a> - Invoice line items
+      <ul>
+        <li>invnum - (multiple) key
+        <li>pkgnum - <a href="#cust_pkg">package</a> or 0 for the special virtual sales tax package
+        <li>setup - setup fee 
+        <li>recur - recurring fee
+        <li>sdate - starting date
+        <li>edate - ending date
+        <li>itemdesc - Line item description (currently used only when pkgnum is 0)
+      </ul>
+    <li><a name="cust_bill_pkg_detail" href="man/FS/cust_bill_pkg_detail.html">cust_bill_pkg_detail</a> - Invoice line items detail
+      <ul>
+        <li>detailnum - primary key
+        <li>pkgnum -
+        <li>invnum - 
+        <li>detail - Detail description
+      </ul>
+    <li><a name="cust_credit" href="man/FS/cust_credit.html">cust_credit</a> - Credits.  The equivalent of a negative <a href="#cust_bill">cust_bill</a> record.
+      <ul>
+        <li>crednum - primary key
+        <li>custnum - <a href="#cust_main">customer</a>
+        <li>amount - amount credited
+        <li>_date
+        <li>otaker - order taker
+        <li>reason
+        <li>closed - books closed flag, empty or `Y'
+      </ul>
+    <li><a name="cust_credit_bill" href="man/FS/cust_credit_bill.html">cust_credit_bill</a> - Credit invoice application.  Links a credit to an invoice.
+      <ul>
+        <li>creditbillnum - primary key
+        <li>crednum - <a href="#cust_credit">credit</a> being applied
+        <li>invnum - <a href="#cust_bill">invoice</a> to which credit is applied
+        <li>amount - amount applied
+        <li>_date
+      </ul>
+    <li><a name="cust_main" href="man/FS/cust_main.html">cust_main</a> - Customers
+      <ul>
+        <li>custnum - primary key
+        <li>agentnum - <a href="#agent">agent</a>
+        <li>refnum - <a href="#part_referral">referral</a>
+        <li>first - name
+        <li>last - name
+        <li>ss - social security number
+        <li>company
+        <li>address1
+        <li>address2
+        <li>city
+        <li>county
+        <li>state
+        <li>zip
+        <li>country
+        <li>daytime - phone
+        <li>night - phone
+        <li>fax - phone
+        <li><i>ship_first</i>
+        <li><i>ship_last</i>
+        <li><i>ship_company</i>
+        <li><i>ship_address1</i>
+        <li><i>ship_address2</i>
+        <li><i>ship_city</i>
+        <li><i>ship_county</i>
+        <li><i>ship_state</i>
+        <li><i>ship_zip</i>
+        <li><i>ship_country</i>
+        <li><i>ship_daytime</i>
+        <li><i>ship_night</i>
+        <li><i>ship_fax</i>
+        <li>payby - CARD, DCHK, CHEK, DCHK, LECB, BILL, or COMP
+        <li>payinfo - card number, P.O.#, or comp issuer
+        <li>paydate - expiration date
+        <li>payname - billing name (name on card)
+        <li>tax - tax exempt, Y or null
+        <li>otaker - order taker
+        <li>referral_custnum
+        <li>comments
+      </ul>
+      (columns in <i>italics</i> are optional)
+    <li><a name="cust_main_invoice" href="man/FS/cust_main_invoice.html">cust_main_invoice</a> - Invoice destinations for email invoices.  Note that a customer can have many email destinations for their invoice (either literal or via svcnum), but only one postal destination.
+      <ul>
+        <li>destnum - primary key
+        <li>custnum - <a href="#cust_main">customer</a>
+        <li>dest - Invoice destination.  Freeside supports three types of invoice delivery: send directly to a service defined in Freeside, send to an arbitrary email address, or print the invoice to a printer and have someone send it out via snail mail.  Freeside determines which method to use based on the contents of the dest field.  If the contents are numeric, a <a href="#svc_acct">svcnum</a> pointing to a valid service is expected in the field.  If the contents are a string, a literal email address is expected to be in the field.  If the special keyword `POST' is present, the snail mail method is used (which is the default if no cust_main_invoice records exist).  Snail mail invoices get their address information from <A name="#cust_main">cust_main</A> and are printed with the printer defined in the configuration files.
+      </ul>
+    <li><a name="cust_main_county" href="man/FS/cust_main_county.html">cust_main_county</a> - Tax rates
+      <ul>
+        <li>taxnum - primary key
+        <li>state
+        <li>county
+        <li>country
+        <li>tax - % rate
+        <li>taxclass
+        <li>exempt_amount
+        <li>taxname - if defined, printed on invoices instead of "Tax"
+      </ul>
+    <li><a name="cust_tax_exempt" href="man/FS/cust_tax_exempt.html">cust_tax_exempt</a> - Tax exemption record
+      <ul>
+        <li>exemptnum - primary key
+        <li>taxnum - <a href="#cust_main_county">tax rate</a>
+        <li>year
+        <li>month
+        <li>amount
+      </ul>
+    <li><a name="cust_pay" href="man/FS/cust_pay.html">cust_pay</a> - Payments.  Money being transferred from a customer.
+      <ul>
+        <li>paynum - primary key
+        <li>custnum - <a href="#cust_main">customer</a>
+        <li>paid - amount
+        <li>_date
+        <li>payby - CARD, CHEK, LECB, BILL, or COMP
+        <li>payinfo - card number, P.O.#, or comp issuer
+        <li>paybatch - text field for tracking card processor batches
+        <li>closed - books closed flag, empty or `Y'
+      </ul>
+    <li><a name="cust_bill_pay" href="man/FS/cust_bill_pay.html">cust_bill_pay</a> - Applicaton of a payment to a specific invoice.
+      <ul>
+        <li>billpaynum
+        <li>invnum - <a href="#cust_bill">invoice</a>
+        <li>paynum - <a href="#cust_pay">payment</a>
+        <li>amount
+        <li>_date
+      </ul>
+    <li><a name="cust_pay_batch" href="man/FS/cust_pay_batch.html">cust_pay_batch</a> - Pending batch
+      <ul>
+        <li>paybatchnum
+        <li>cardnum
+        <li>exp - card expiration
+        <li>amount
+        <li>invnum - <a href="#cust_bill">invoice</a>
+        <li>custnum - <a href="#cust_main">customer</a>
+        <li>payname - name on card
+        <li>first - name
+        <li>last - name
+        <li>address1
+        <li>address2
+        <li>city
+        <li>state
+        <li>zip
+        <li>country
+      </ul>
+    <li><a name="cust_pkg" href="man/FS/cust_pkg.html">cust_pkg</a> - Customer billing items
+      <ul>
+        <li>pkgnum - primary key
+        <li>custnum - <a href="#cust_main">customer</a>
+        <li>pkgpart - <a href="#part_pkg">Package definition</a>
+        <li>setup - date
+        <li>bill - next bill date
+        <li>last_bill - last bill date
+        <li>susp - (past) suspension date
+        <li>expire - (future) cancellation date
+        <li>cancel - (past) cancellation date
+        <li>otaker - order taker
+        <li>manual_flag - If this field is set to 1, disables the automatic unsuspensiond of this package when using the <a href="config.html#unsuspendauto">unsuspendauto</a> config file.
+      </ul>
+    <li><a name="cust_refund" href="man/FS/cust_refund.html">cust_refund</a> - Refunds.  The transfer of money to a customer; equivalent to a negative <a href="#cust_pay">cust_pay</a> record.
+      <ul>
+        <li>refundnum - primary key
+        <li>custnum - <a href="#cust_main">customer</a>
+        <li>refund - amount
+        <li>_date
+        <li>payby - CARD, CHEK, LECB, BILL or COMP
+        <li>payinfo - card number, P.O.#, or comp issuer
+        <li>otaker - order taker
+        <li>closed - books closed flag, empty or `Y'
+      </ul>
+    <li><a name="cust_credit_refund" href="man/FS/cust_credit_refund.html">cust_credit_refund</a> - Applicaton of a refund to a specific credit.
+      <ul>
+        <li>creditrefundnum - primary key
+        <li>crednum - <a href="#cust_credit">credit</a>
+        <li>refundnum - <a href="#cust_refund">refund</a>
+        <li>amount
+        <li>_date
+      </ul>
+    <li><a name="cust_svc" href="man/FS/cust_svc.html">cust_svc</a> - Customer services
+      <ul>
+        <li>svcnum - primary key
+        <li>pkgnum - <a href="#cust_pkg">package</a>
+        <li>svcpart - <a href="#part_svc">Service definition</a>
+      </ul>
+    <li><a name="nas" href="man/FS/nas.html">nas</a> - Network Access Server (terminal server)
+      <ul>
+        <li>nasnum - primary key
+        <li>nas - NAS name
+        <li>nasip - NAS ip address
+        <li>nasfqdn - NAS fully-qualified domain name
+        <li>last - timestamp indicating the last instant the NAS was in a known state (used by the session monitoring).
+      </ul>
+    <li><a name="part_pkg" href="man/FS/part_pkg.html">part_pkg</a> - Package definitions
+      <ul>
+        <li>pkgpart - primary key
+        <li>pkg - package name
+        <li>comment - non-customer visable package comment
+        <li>setup - setup fee expression
+        <li>freq - recurring frequency (months)
+        <li>recur - recurring fee expression
+        <li>setuptax - Setup fee tax exempt flag, empty or `Y'
+        <li>recurtax - Recurring fee tax exempt flag, empty or `Y'
+        <li>plan - price plan
+        <li>plandata - additional price plan data
+        <li>disabled - Disabled flag, empty or `Y'
+      </ul>
+    <li><a name="part_referral" href="man/FS/part_referral.html">part_referral</a> - Referral listing
+      <ul>
+        <li>refnum - primary key
+        <li>referral - referral
+      </ul>
+    <li><a name="part_svc" href="man/FS/part_svc.html">part_svc</a> - Service definitions
+      <ul>
+        <li>svcpart - primary key
+        <li>svc - name of this service
+        <li>svcdb - table used for this service: svc_acct, svc_forward, svc_domain, svc_charge or svc_wo
+        <li>disabled - Disabled flag, empty or `Y'
+<!--        <li><i>table</i>__<i>field</i> - Default or fixed value for <i>field</i> in <i>table</i>
+        <li><i>table</i>__<i>field</i>_flag - null, D or F
+-->
+      </ul>
+    <li><a name="part_svc_column" href="man/FS/part_svc_column.html">part_svc_column</a>
+      <ul>
+        <li>columnnum - primary key
+        <li>svcpart - <a href="#part_svc">Service definition</a>
+        <li>columnname - column name in part_svc.svcdb table
+        <li>columnvalue - default or fixed value for the column
+        <li>columnflag - null, D or F
+      </ul>
+    <li><a name="pkg_svc" href="man/FS/pkg_svc.html">pkg_svc</a>
+      <ul>
+        <li>pkgpart - <a href="#part_pkg">Package definition</a>
+        <li>svcpart - <a href="#part_svc">Service definition</a>
+        <li>quantity - quantity of this service that this package includes
+      </ul>
+    <li><a name="export_svc" href="man/FS/export_svc.html">export_svc</a>
+      <ul>
+        <li>exportsvcnum - primary key
+        <li>svcpart - <a href="#part_svc">Service definition</a>
+        <li>exportnum - <a href="#exportnum">Export</a>
+      </ul>
+    <li><a name="part_export" href="man/FS/part_export.html">part_export</a> - Export to external provisioning
+      <ul>
+        <li>exportnum - primary key
+        <li>machine - Machine name 
+        <li>exporttype - Export type
+        <li>nodomain - blank or Y: usernames are exported to this service with no domain
+      </ul>
+    <li><a name="part_export_option" href="man/FS/part_export_option.html">part_export_option</a> - provisioning options
+      <ul>
+        <li>optionnum - primary key
+        <li>exportnum - <a href="#part_export">Export</a>
+        <li>optionname - option name
+        <li>optionvalue - option value
+      </ul>
+    <li><a name="port" href="man/FS/port.html">port</a> - individual port on a <a href="#nas">nas</a>
+      <ul>
+        <li>portnum - primary key
+        <li>ip - IP address of this port
+        <li>nasport - port number on the NAS
+        <li>nasnum - <a href="#nas">NAS</a>
+      </ul>
+    <li><a name="prepay_credit" href="man/FS/prepay_credit.html">prepay_credit</a>
+      <ul>
+        <li>prepaynum - primary key
+        <li>identifier - text or numeric string used to receive this credit
+        <li>amount - amount of credit
+      </ul>
+    <li><a name="session" href="man/FS/session.html">session</a>
+      <ul>
+        <li>sessionnum - primary key
+        <li>portnum - <a href="#port">Port</a>
+        <li>svcnum - <a href="#svc_acct">Account</a>
+        <li>login - timestamp indicating the beginning of this user session.
+        <li>logout - timestamp indicating the end of this user session.  May be null, which indicates a currently open session.
+      </ul>
+
+    <li><a name="svc_acct" href="man/FS/svc_acct.html">svc_acct</a> - Accounts
+      <ul>
+        <li>svcnum - <a href="#cust_svc">primary key</a>
+        <li>username
+        <li>_password
+        <li>sec_phrase - security phrase
+        <li>popnum - <a href="#svc_acct_pop">Point of Presence</a>
+        <li>uid
+        <li>gid
+        <li>finger - GECOS
+        <li>dir
+        <li>shell
+        <li>quota - (unimplementd)
+        <li>slipip - IP address
+        <li>seconds
+        <li>domsvc
+        <li>radius_<i>Radius_Reply_Attribute</i> - Radius-Reply-Attribute
+        <li>rc_<i>Radius_Check_Attribute</i> - Radius-Check-Attribute
+      </ul>
+    <li><a name="svc_acct_pop" href="man/FS/svc_acct_pop.html">svc_acct_pop</a> - Points of Presence
+      <ul>
+        <li>popnum - primary key
+        <li>city
+        <li>state
+        <li>ac - area code
+        <li>exch - exchange
+        <li>loc - rest of number
+      </ul>
+    <li><a name="part_pop_local" href="man/FS/part_pop_local.html">part_pop_local</a> - Local calling areas
+      <ul>
+        <li>localnum - primary key
+        <li>popnum - primary key
+        <li>city
+        <li>state
+        <li>npa - area code
+        <li>nxx - exchange
+      </ul>
+    <li><a name="svc_domain" href="man/FS/svc_domain.html">svc_domain</a> - Domains
+      <ul>
+        <li>svcnum - <a href="#cust_svc">primary key</a>
+        <li>domain
+      </ul>
+    <li><a name="svc_forward" href="man/FS/svc_forward.html">svc_forward</a> - Mail forwarding aliases
+      <ul>
+        <li>svcnum - <a href="#cust_svc">primary key</a>
+        <li>srcsvc - <a href="#svc_acct">svcnum of the source of this forward</a>
+        <li>dstsvc - <a href="#svc_acct">svcnum of the destination of this forward</a>
+        <li>dst - foreign destination (email address) - forward not local to freeside
+      </ul>
+    <li><a name="domain_record" href="man/FS/domain_record.html">domain_record</a> - Domain zone detail
+      <ul>
+        <li>recnum - primary key
+        <li>svcnum - <a href="#svc_domain">Domain</a> (by svcnum)
+        <li>reczone - zone for this line
+        <li>recaf - address family, usually <b>IN</b>
+        <li>rectype - type for this record (<b>A</b>, <b>MX</b>, etc.)
+        <li>recdata - data for this record
+      </ul>
+    <li><a name="svc_www" href="man/FS/svc_www.html">svc_www</a>
+      <ul>
+       <li>svcnum - <a href="#cust-svc">primary key</a>
+       <li>recnum - <a href="#domain_record">host</a>
+       <li>usersvc - <a href="#svc_acct">account</a>
+      </ul>
+    <li><a name="type_pkgs" href="man/FS/type_pkgs.html">type_pkgs</a>
+      <ul>
+        <li>typenum - <a href="#agent_type">agent type</a>
+        <li>pkgpart - <a href="#part_pkg">Package definition</a>
+      </ul>
+    <li><a name="queue" href="man/FS/queue.html">queue</a> - job queue
+      <ul>
+        <li>jobnum - primary key
+        <li>job
+        <li>_date
+        <li>status
+        <li>statustext
+        <li>svcnum
+      </ul>
+    <li><a name="queue_arg" href="man/FS/queue_arg.html">queue_arg</a> - job arguments
+      <ul>
+        <li>argnum - primary key
+        <li>jobnum - <a href="#queue">job</a>
+        <li>arg - argument
+      </ul>
+    <li><a name="queue_depend" href="man/FS/queue_depend.html">queue_depend</a> - job dependancies
+      <ul>
+        <li>dependnum - primary key
+        <li>jobnum - source jobnum
+        <li>depend_jobnum - dependancy jobnum
+      </ul>
+    <li><a name="radius_usergroup" href="man/FS/radius_usergroup.html">radius_usergroup</a> - Link users to RADIUS groups.
+      <ul>
+        <li>usergroupnum - primary key
+        <li>svcnum - <a href="#svc_acct">account</a>
+        <li>groupname
+      </ul>
+    <li><a name="msgcat" href="man/FS/msgcat.html">msgcat</a> - i18n message catalog
+      <ul>
+        <li>msgnum - primary key
+        <li>msgcode - message code
+        <li>locale - locale
+        <li>msg - Message text
+      </ul>
+  </ul>
+</body>
diff --git a/httemplate/docs/schema.png b/httemplate/docs/schema.png
new file mode 100644 (file)
index 0000000..d0392e7
Binary files /dev/null and b/httemplate/docs/schema.png differ
diff --git a/httemplate/docs/session.html b/httemplate/docs/session.html
new file mode 100644 (file)
index 0000000..72e1642
--- /dev/null
@@ -0,0 +1,59 @@
+<head>
+  <title>Session monitor</title>
+</head>
+<body>
+<h1>Session monitor</h1>
+<h2>Installation</h2>
+For security reasons, the client portion of the session montior may run on one
+or more external public machine(s).  On these machines, install:
+<ul>
+  <li><a href="http://www.perl.com/CPAN/doc/relinfo/INSTALL.html">Perl</a> (at l
+east 5.004_05 for the 5.004 series or 5.005_03 for the 5.005 series.  Don't enable experimental features like threads or the PerlIO abstraction layer.)
+  <li><a href="man/FS/SessionClient.html">FS::SessionClient</a> (copy the fs_session/FS-SessionClient directory to the external machine, then: perl Makefile.PL; make; make install)
+</ul>
+Then:
+<ul>
+  <li>Add the user `freeside' to the the external machine.
+  <li>Create the /usr/local/freeside directory on the external machine (owned by the freeside user).
+  <li>touch /usr/local/freeside/fs_sessiond_socket; chown freeside /usr/local/freeside/fs_sessiond_socket; chmod 600 /usr/local/freeside/fs_sessiond_socket
+    <li>Append the identity.pub from the freeside user on your freeside machine to the authorized_keys file of the newly created freeside user on the external machine(s).
+  <li>Run <pre>fs_session_server <i>user</i> <i>machine</i></pre> on the Freeside machine.
+  <ul>
+    <li><i>user</i> is a user from the mapsecrets file.
+    <li><i>machine</i> is the name of the external machine.
+  </ul>
+</ul>
+<h2>Usage</h2>
+<ul>
+  <li>Web
+    <ul>
+      <li>Copy FS-SessionClient/cgi/login.cgi and logout.cgi to your web
+          server's document space.  
+      <li>Use <a href="http://www.apache.org/docs/suexec.html">suEXEC</a> or <a href="http://www.perl.com/CPAN-local/doc/manual/html/pod/perlsec.html#Security_Bugs">setuid</a> (see <a href="install.html">install.html</a> for details) to run login.cgi and logout.cgi as the freeside user.
+    </ul>
+  <li>Command-line
+    <br><pre>freeside-login username ( portnum | ip | nasnum nasport )
+freeside-logout username ( portnum | ip | nasnum nasport )</pre>
+    <ul>
+      <li><i>username</i> is a customer username from the svc_acct table
+      <li><i>portnum</i>, <i>ip</i> or <i>nasport</i> and <i>nasnum</i> uniquely identify a port in the <a href="schema.html#port">port</a> database table.
+    </ul>
+  <li>RADIUS - One of:
+    <ul>
+      <li>Run the <b>freeside-sqlradius-radacctd</b> daemon to import radacct
+        records from all configured sqlradius exports:
+          <tt>freeside-sqlradius-radacctd username</tt>
+      <li>Configure your RADIUS server's login and logout callbacks to use the command-line <tt>freeside-login</tt> and <tt>freeside-logout</tt> utilites.
+      <li> <i>(incomplete)</i>Use the <b>fs_radlog/fs_radlogd</b> tool to
+        import records from a text radacct file.
+    </ul>
+</ul>
+<h2>Callbacks</h2>
+<ul>
+  <li>Sesstion start - The command(s) specified in the <a href="config.html#session-start">session-start</a> configuration file are executed on the Freeside machine.  The contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$ip</code>, <code>$nasip</code> and <code>$nasfqdn</code>, which are the IP address of the starting session, and the IP address and fully-qualified domain name of the NAS this session is on.
+  <li>Session end - The command(s) specified in the <a href="config.html#session-stop">session-stop</a> configuration file are executed on the Freeside machine.  The contents of the file are treated as a double-quoted perl string, with the following variables available: <code>$ip</code>, <code>$nasip</code> and <code>$nasfqdn</code>, which are the IP address of the starting session, and the IP address and fully-qualified domain name of the NAS this session is on.
+</ul>
+<h2>Dropping expired users</h2>
+Run <pre>bin/freeside-session-kill username</pre> periodically from cron.
+</body>
+</html>
diff --git a/httemplate/docs/signup.html b/httemplate/docs/signup.html
new file mode 100644 (file)
index 0000000..5168f47
--- /dev/null
@@ -0,0 +1,56 @@
+<head>
+  <title>Signup server</title>
+</head>
+<body>
+  <h1>Signup server</h1>
+For security reasons, the signup server should run on an external public
+webserver.  On this machine, install:
+<ul>
+  <li>A web server, such as <a href="http://www.apache-ssl.org">Apache-SSL</a> or <a href="http://www.apache.org">Apache</a>
+  <li><a href="ftp://ftp.cs.hut.fi/pub/ssh/">SSH</a>
+  <li><a href="http://www.perl.com/CPAN/doc/relinfo/INSTALL.html">Perl</a> (at least 5.004_05 for the 5.004 series or 5.005_03 for the 5.005 series.  Don't enable experimental features like threads or the PerlIO abstraction layer.)
+  <li><a href="http://search.cpan.org/search?dist=Text-Template">Text::Template</a>
+  <li><a href="http://search.cpan.org/search?dist=Storable">Storable</a>
+  <li><a href="http://search.cpan.org/search?dist=Business-CreditCard">Business-CreditCard</a>
+  <li><a href="http://www.sisd.com/useragent">HTTP::Headers::UserAgent</a> (version 2.0 or higher; not yet indexed correctly on CPAN)
+
+  <li><a href="man/FS/SignupClient.html">FS::SignupClient</a> (copy the fs_signup/FS-SignupClient directory to the external machine, then: perl Makefile.PL; make; make install)
+</ul>
+Then:
+<ul>
+  <li>Add the user `freeside' to the the external machine.
+  <li>Copy or symlink fs_signup/FS-SignupClient/cgi/signup.cgi into the web server's document space.
+  <li>When linking to signup.cgi, you can include a referring custnum in the URL as follows: <code>http://public.web.server/path/signup.cgi?ref=1542</code>
+  <li>Enable CGI execution for files with the `.cgi' extension.  (with <a href="http://www.apache.org/docs/mod/mod_mime.html#addhandler">Apache</a>)
+  <li>Create the /usr/local/freeside directory on the external machine (owned by the freeside user).
+  <li>touch /usr/local/freeside/fs_signupd_socket; chown freeside /usr/local/freeside/fs_signupd_socket; chmod 600 /usr/local/freeside/fs_signupd_socket
+  <li>Use <a href="http://www.apache.org/docs/suexec.html">suEXEC</a> or <a href="http://www.perl.com/CPAN-local/doc/manual/html/pod/perlsec.html#Security_Bugs">setuid</a> (see <a href="install.html">install.html</a> for details) to run signup.cgi as the freeside user.
+  <li>Append the identity.pub from the freeside user on your freeside machine to the authorized_keys file of the newly created freeside user on the external machine(s).
+  <li>Run <pre>fs_signup_server <i>user</i> <i>machine</i> <i>agentnum</i> <i>refnum</i></pre> on the Freeside machine.
+  <ul>
+    <li><i>user</i> is a user from the mapsecrets file.
+    <li><i>machine</i> is the name of the external machine.
+    <li><i>agentnum</i> and <i>refnum</i> are the <a href="schema.html#agent">agent</a> and <a href="schema.html#part_referral">referral</a>, respectively, to use for customers who sign up via this signup server.
+  </ul>
+</ul>
+Optional:
+<ul>
+  <li>If you create a <b>/usr/local/freeside/ieak.template</b> file on the external machine, it will be sent to IE users with MIME type <i>application/x-Internet-signup</i>.  This file will be processed with <a href="http://search.cpan.org/doc/MJD/Text-Template-1.23/Template.pm">Text::Template</a> with the variables listed below available.
+  (an example file is included as <b>fs_signup/ieak.template</b>)  See the <a href="http://www.microsoft.com/windows/ieak/techinfo/deploy/60/en/toc.asp">IEAK documentation</a> for more information.
+  <li>If you create a <b>/usr/local/freeside/cck.template</b> file on the external machine, the variables defined will be sent to Netscape users with MIME type <i>application/x-netscape-autoconfigure-dialer-v2</i>.  This file will be processed with <a href="http://search.cpan.org/doc/MJD/Text-Template-1.23/Template.pm">Text::Template</a> with the variables listed below available.
+  (an example file is included as <b>fs_signup/cck.template</b>).  See the <a href="http://help.netscape.com/products/client/mc/acctproc4.html">Netscape documentation</a> for more information.
+  <li>If you create a <b>/usr/local/freeside/success.html</b> file on the external machine, it will be used as the success HTML page.  Although template substiutions are available, a regular HTML file will work fine here, unlike signup.html.  An example file is included as <b>fs_signup/FS-SignupClient/cgi/success.html</b>
+  <li>Variable substitutions available in <b>ieak.template</b>, <b>cck.template</b> and <b>success.html</b>:
+    <ul>
+      <li>$ac - area code of selected POP
+      <li>$exch - exchange of selected POP
+      <li>$loc - local part of selected POP
+      <li>$username
+      <li>$password
+      <li>$email_name - first and last name
+      <li>$pkg - package name
+    </ul>
+  <li>If you create a <b>/usr/local/freeside/signup.html</b> file on the external machine, it will be used as a template for the form HTML.  This requires the template to be constructed appropriately; probably best to start with the example file included as <b>fs_signup/FS-SignupClient/cgi/signup.html</b>.
+  <li>If there are any entries in the <i>prepay_credit</i> table, a user can enter a string matching the <b>identifier</i> column to receive the credit specified in the <b>amount</b> column, and/or the time specified in the <b>seconds</b> column (for use with the <a href="session.html">session monitor</a>), after which that <b>identifier</b> is no longer valid.  This can be used to implement pre-paid "calling card" type signups.  The <i>bin/generate-prepay</i> script can be used to populate the <i>prepay_credit</i> table.
+</ul>
+</body>
diff --git a/httemplate/docs/ssh.html b/httemplate/docs/ssh.html
new file mode 100755 (executable)
index 0000000..d2c501e
--- /dev/null
@@ -0,0 +1,16 @@
+<head>
+  <title>Unattended SSH</title>
+</head>
+<body>
+  <h1>Unattended SSH</h1>
+  <br><a name=ssh>Unattended remote login</a> - Freeside can login to remote machines unattended using SSH.  This can pose a security risk if not configured correctly, and will allow an intruder who breaks into your freeside machine full access to your remote machines.  <b>Do not use this feature unless you understand what you are doing!</b>
+    <ul>
+      <li>As the freeside user (on your freeside machine), generate an authentication key using <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>.  Since this is for unattended operation, use a blank passphrase.
+      <li>Append the newly-created <code>identity.pub</code> file to <code>~root/.ssh/authorized_keys</code> (or the appopriate <code>~username/.ssh/authorized_keys</code>) on the remote machine(s).
+      <li>Some new SSH v2 implementation accept v2 style keys only.  Use the <code>-t</code> option to <a href="http://www.tac.eu.org/cgi-bin/man-cgi?ssh-keygen+1">ssh-keygen</a>, and append the created <code>id_dsa.pub</code> or <code>id_rsa.pub</code> to <code>~root/.ssh/authorized_keys2</code> (or the appopriate <code>~username/.ssh/authorized_keys</code>) on the remote machine(s).
+      <li>You may need to set <code>PermitRootLogin without-password</code> (meaning with keys only) in your <code>sshd_config</code> file on the remote machine(s).
+      <li>You may want to set <code>ForwardX11 = no</code> in <code>~root/.ssh/config</code> to prevent spurious errors if your distribution turns on X11 forwarding by default.
+    </ul>
+
+</body>
+
diff --git a/httemplate/docs/trouble.html b/httemplate/docs/trouble.html
new file mode 100755 (executable)
index 0000000..fce7439
--- /dev/null
@@ -0,0 +1,26 @@
+<head>
+  <title>Troubleshooting</title>
+</head>
+<body>
+  <h1>Troubleshooting</h1>
+  <ul>
+    <li>When troubleshooting the web interface, helpful information is often in your web server's error log.
+    <li>If bin/svc_acct.import fails with an "Out of memory!" error using MySQL, upgrede MySQL and recompile the Perl DBD.  There was a memory leak in some older versions of MySQL.
+    <li>If you get tons of errors in your web server's error log like this:
+<pre>
+Ambiguous use of value => resolved to "value" =>
+at /usr/lib/perl5/site_perl/File/CounterFile.pm line 132.
+</pre>
+        This clutters up your log files but is otherwise harmless.  Upgrade to the latest File::CounterFile. 
+    <li>If you get errors like this:
+<pre>
+UID.pm: Can't open /var/spool/freeside/conf/secrets: Permission denied 
+at <i>/your/path</i>/site_perl/FS/UID.pm line 26.
+BEGIN failed--compilation aborted at
+<i>/your/path</i>/edit/process/part_svc.cgi line 15.
+</pre>
+        Then the scripts are not running as the freeside freeside user.  See
+the <a href="install.html">New Installation</a> section of the documentation.
+  <li>If you receive `can not connect to server' errors using MySQL on a system that doesn't support native threading, you may need to specify the full hostname in your DBI datasource.  See the <a href="http://www.mysql.com/Manual_chapter/manual_Problems.html#Can_not_connect_to_server">MySQL documentation</a>, DBI manpage and the DBD::mysql manpage for details.
+  </ul>
+</body>
diff --git a/httemplate/docs/upgrade10.html b/httemplate/docs/upgrade10.html
new file mode 100644 (file)
index 0000000..1035510
--- /dev/null
@@ -0,0 +1,120 @@
+<pre>
+this is incomplete
+
+install DBIx::DBSchema 0.21
+
+install NetAddr::IP and Chart::Base
+
+CREATE TABLE cust_bill_pkg_detail (
+  detailnum serial,
+  pkgnum int NOT NULL,
+  invnum int NOT NULL,
+  detail varchar(80),
+  PRIMARY KEY (detailnum)
+);
+CREATE INDEX cust_bill_pkg_detail1 ON cust_bill_pkg_detail ( pkgnum, invnum );
+
+CREATE TABLE router (
+  routernum serial,
+  routername varchar(80),
+  svcnum int,
+  PRIMARY KEY (routernum)
+);
+
+CREATE TABLE part_svc_router (
+  svcpart int NOT NULL,
+  routernum int NOT NULL
+);
+
+CREATE TABLE part_router_field (
+  routerfieldpart serial,
+  name varchar(80),
+  length int NOT NULL,
+  check_block text,
+  list_source text,
+  PRIMARY KEY (routerfieldpart)
+);
+
+CREATE TABLE router_field (
+  routerfieldpart int NOT NULL,
+  routernum int NOT NULL,
+  value varchar(128)
+);
+CREATE UNIQUE INDEX router_field1 ON router_field ( routerfieldpart, routernum );
+
+CREATE TABLE addr_block (
+  blocknum serial,
+  routernum int NOT NULL,
+  ip_gateway varchar(15) NOT NULL,
+  ip_netmask int NOT NULL,
+  PRIMARY KEY (blocknum)
+);
+CREATE UNIQUE INDEX addr_block1 ON addr_block ( blocknum, routernum );
+
+CREATE TABLE part_sb_field (
+  sbfieldpart serial,
+  svcpart int NOT NULL,
+  name varchar(80) NOT NULL,
+  length int NOT NULL,
+  check_block text NULL,
+  list_source text NULL,
+  PRIMARY key (sbfieldpart)
+);
+CREATE UNIQUE INDEX part_sb_field1 ON part_sb_field ( sbfieldpart, svcpart );
+
+CREATE TABLE sb_field (
+  sbfieldpart int NOT NULL,
+  svcnum int NOT NULL,
+  value varchar(128)
+);
+CREATE UNIQUE INDEX sb_field1 ON sb_field ( sbfieldpart, svcnum );
+
+CREATE TABLE svc_broadband (
+  svcnum int NOT NULL,
+  blocknum int NOT NULL,
+  speed_up int NOT NULL,
+  speed_down int NOT NULL,
+  ip_addr varchar(15),
+  PRIMARY KEY (svcnum)
+);
+
+DELETE INDEX cust_bill_pkg1;
+
+ALTER TABLE cust_bill_pkg ADD itemdesc varchar(80) NULL;
+ALTER TABLE h_cust_bill_pkg ADD itemdesc varchar(80) NULL;
+ALTER TABLE cust_main_county ADD taxname varchar(80) NULL;
+ALTER TABLE h_cust_main_county ADD taxname varchar(80) NULL;
+ALTER TABLE cust_pkg ADD last_bill int NULL;
+ALTER TABLE h_cust_pkg ADD last_bill int NULL;
+
+dump database, edit:
+- cust_main: increase otaker from 8 to 32
+- cust_main: change ss from char(11) to varchar(11)
+- cust_credit: increase otaker from 8 to 32
+- cust_pkg: increase otaker from 8 to 32
+- cust_refund: increase otaker from 8 to 32
+- domain_record: increase reczone from 80 to 255
+- domain_record: change rectype from char to varchar
+- domain_record: increase recdata from 80 to 255
+then reload
+
+optionally:
+
+  CREATE INDEX cust_main6 ON cust_main ( daytime );
+  CREATE INDEX cust_main7 ON cust_main ( night );
+  CREATE INDEX cust_main8 ON cust_main ( fax );
+  CREATE INDEX cust_main9 ON cust_main ( ship_daytime );
+  CREATE INDEX cust_main10 ON cust_main ( ship_night );
+  CREATE INDEX cust_main11 ON cust_main ( ship_fax );
+
+  serial columns
+
+mandatory again:
+
+dbdef-create username
+create-history-tables username cust_bill_pkg_detail router part_svc_router part_router_field router_field addr_block part_sb_field sb_field svc_broadband
+dbdef-create username
+
+
+
+</pre>
diff --git a/httemplate/docs/upgrade7.html b/httemplate/docs/upgrade7.html
new file mode 100644 (file)
index 0000000..d9dcfe2
--- /dev/null
@@ -0,0 +1,24 @@
+<head>
+  <title>Upgrading to 1.3.1</title>
+</head>
+<body>
+<h1>Upgrading to 1.3.1 from 1.3.0</h1>
+<ul>
+  <li>If migrating from 1.0.0, see these <a href="upgrade.html">instructions</a> first.
+  <li>If migrating from less than 1.1.4, see these <a href="upgrade2.html">instructions</a> first.
+  <li>If migrating from less than 1.2.0, see these <a href="upgrade3.html">instructions</a> first.
+  <li>If migrating from less than 1.2.2, see these <a href="upgrade4.html">instructions</a> first.
+  <li>If migrating from less than 1.2.3, see these <a href="upgrade5.html">instructions</a> first.
+  <li>If migrating from less than 1.3.0, see these <a href="upgrade6.html">instructions</a> first.
+  <li>Back up your data and current Freeside installation.
+  <li>Copy or symlink htdocs to the new copy.
+  <li>Change to the FS directory in the new tarball, and build and install the
+      Perl modules:
+    <pre>
+$ cd FS/
+$ perl Makefile.PL
+$ make
+$ su
+# make install UNINST=1</pre>
+  <li>Run bin/dbdef-create.
+</body>
diff --git a/httemplate/docs/upgrade8.html b/httemplate/docs/upgrade8.html
new file mode 100644 (file)
index 0000000..cf60a85
--- /dev/null
@@ -0,0 +1,392 @@
+<head>
+  <title>Upgrading to 1.4.0</title>
+</head>
+<body>
+<h1>Upgrading to 1.4.0 from 1.3.1</h1>
+<ul>
+  <li>If migrating from less than 1.3.1, see these <a href="upgrade7.html">instructions</a> first.
+  <li><font size="+2" color="#ff0000">Backup your database and current Freeside installation.</font> (with&nbsp;<a href="http://www.ca.postgresql.org/devel-corner/docs/postgres/backup.html">PostgreSQL</a>) (with&nbsp;<a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Backup">MySQL</a>)
+  <li><a href="http://perl.apache.org/">mod_perl</a> is now required.
+  <li>Install <a href="http://search.cpan.org/search?dist=Time-Duration">Time-Duration</a>, <a href="http://search.cpan.org/search?dist=Tie-IxHash">Tie-IxHash</a> and <a href="http://search.cpan.org/search?dist=HTML-Widgets-SelectLayers">HTML-Widgets-SelectLayers</a> (minimum version 0.02).
+  <li>Install <a href="http://www.apache-asp.org/">Apache::ASP</a> or <a href="http://www.masonhq.com/">HTML::Mason</a> (use version 1.0x - Freeside is not yet compatible with version 1.1x).
+  <li>Install <a href="http://rsync.samba.org/">rsync</a>
+</ul>
+<table>
+  <tr>
+    <th>Apache::ASP</th><th>Mason</th>
+  </tr>
+  <tr>
+    <td><ul>
+      <li>Run <tt>make aspdocs</tt>
+      <li>Copy <tt>aspdocs/</tt> to your web server's document space.
+      <li>Create a <a href="http://www.apache-asp.org/config.html#Global">Global</a> directory, such as <tt>/usr/local/etc/freeside/asp-global/</tt>
+      <li>Copy <tt>htetc/global.asa</tt> to the Global directory.
+      <li>Configure Apache for the Global directory and to execute .cgi files using Apache::ASP.  For example:
+<font size="-1"><pre>
+&lt;Directory /usr/local/apache/htdocs/freeside-asp&gt;
+&lt;Files ~ (\.cgi)&gt;
+AddHandler perl-script .cgi
+PerlHandler Apache::ASP
+&lt;/Files&gt;
+&lt;Perl&gt;
+$MLDBM::RemoveTaint = 1;
+&lt;/Perl&gt;
+PerlSetVar Global /usr/local/etc/freeside/asp-global/
+&lt;/Directory&gt;
+</pre></font>
+    </ul></td>
+    <td><ul>
+      <li>(use version 1.0x - Freeside is not yet compatible with version 1.1x)
+      <li>Run <tt>make masondocs</tt>
+      <li>Copy <tt>masondocs/</tt> to your web server's document space.
+      <li>Copy <tt>htetc/handler.pl</tt> to your web server's configuration directory.
+      <li>Edit <tt>handler.pl</tt> and set an appropriate <tt>data_dir</tt>, such as <tt>/usr/local/etc/freeside/mason-data</tt>
+      <li>Configure Apache to use the <tt>handler.pl</tt> file and to execute .cgi files using HTML::Mason.  For example:
+<font size="-1"><pre>
+&lt;Directory /usr/local/apache/htdocs/freeside-mason&gt;
+&lt;Files ~ (\.cgi)&gt;
+AddHandler perl-script .cgi
+PerlHandler HTML::Mason
+&lt;/Files&gt;
+&lt;Perl&gt;
+require "/usr/local/apache/conf/handler.pl";
+&lt;/Perl&gt;
+&lt;/Directory&gt;
+</pre></font>
+    </ul></td>
+  </tr>
+</table>
+<ul>
+  <li>Build and install the Perl modules:
+    <pre>
+$ su
+# make install-perl-modules</pre>
+   <li>Apply the following changes to your database:
+<pre>
+CREATE TABLE svc_forward (
+  svcnum int NOT NULL,
+  srcsvc int NOT NULL,
+  dstsvc int NOT NULL,
+  dst varchar(80),
+  PRIMARY KEY (svcnum)
+);
+ALTER TABLE part_svc ADD svc_forward__srcsvc varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_forward__srcsvc_flag char(1) NULL;
+ALTER TABLE part_svc ADD svc_forward__dstsvc varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_forward__dstsvc_flag char(1) NULL;
+ALTER TABLE part_svc ADD svc_forward__dst varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_forward__dst_flag char(1) NULL;
+
+CREATE TABLE cust_credit_bill (
+  creditbillnum int primary key,
+  crednum int not null,
+  invnum int not null,
+  _date int not null,
+  amount decimal(10,2) not null
+);
+
+CREATE TABLE cust_bill_pay (
+  billpaynum int primary key,
+  invnum int not null,
+  paynum int not null,
+  _date int not null,
+  amount decimal(10,2) not null
+);
+
+CREATE TABLE cust_credit_refund (
+  creditrefundnum int primary key,
+  crednum int not null,
+  refundnum int not null,
+  _date int not null,
+  amount decimal(10,2) not null
+);
+
+CREATE TABLE part_svc_column (
+  columnnum int primary key,
+  svcpart int not null,
+  columnname varchar(64) not null,
+  columnvalue varchar(80) null,
+  columnflag char(1) null
+);
+
+CREATE TABLE queue (
+  jobnum int primary key,
+  job text not null,
+  _date int not null,
+  status varchar(80) not null,
+  statustext text null,
+  svcnum int null
+);
+CREATE INDEX queue1 ON queue ( svcnum );
+CREATE INDEX queue2 ON queue ( status );
+
+CREATE TABLE queue_arg (
+  argnum int primary key,
+  jobnum int not null,
+  arg text null
+);
+CREATE INDEX queue_arg1 ON queue_arg ( jobnum );
+
+CREATE TABLE queue_depend (
+  dependnum int primary key,
+  jobnum int not null,
+  depend_jobnum int not null
+);
+CREATE INDEX queue_depend1 ON queue_depend ( jobnum );
+CREATE INDEX queue_depend2 ON queue_depend ( depend_jobnum );
+
+CREATE TABLE part_pop_local (
+  localnum int primary key,
+  popnum int not null,
+  city varchar(80) null,
+  state char(2) null,
+  npa char(3) not null,
+  nxx char(3) not null
+);
+CREATE UNIQUE INDEX part_pop_local1 ON part_pop_local ( npa, nxx );
+
+CREATE TABLE cust_bill_event (
+  eventnum int primary key,
+  invnum int not null,
+  eventpart int not null,
+  _date int not null
+);
+CREATE UNIQUE INDEX cust_bill_event1 ON cust_bill_event ( eventpart, invnum );
+CREATE INDEX cust_bill_event2 ON cust_bill_event ( invnum );
+
+CREATE TABLE part_bill_event (
+  eventpart int primary key,
+  payby char(4) not null,
+  event varchar(80) not null,
+  eventcode text null,
+  seconds int null,
+  weight int not null,
+  plan varchar(80) null,
+  plandata text null,
+  disabled char(1) null
+);
+CREATE INDEX part_bill_event1 ON part_bill_event ( payby );
+
+CREATE TABLE export_svc (
+  exportsvcnum int primary key,
+  exportnum int not null,
+  svcpart int not null
+);
+CREATE UNIQUE INDEX export_svc1 ON export_svc ( exportnum, svcpart );
+CREATE INDEX export_svc2 ON export_svc ( exportnum );
+CREATE INDEX export_svc3 ON export_svc ( svcpart );
+
+CREATE TABLE part_export (
+  exportnum int primary key,
+  machine varchar(80) not null,
+  exporttype varchar(80) not null,
+  nodomain char(1) NULL
+);
+CREATE INDEX part_export1 ON part_export ( machine );
+CREATE INDEX part_export2 ON part_export ( exporttype );
+
+CREATE TABLE part_export_option (
+  optionnum int primary key,
+  exportnum int not null,
+  optionname varchar(80) not null,
+  optionvalue text NULL
+);
+CREATE INDEX part_export_option1 ON part_export_option ( exportnum );
+CREATE INDEX part_export_option2 ON part_export_option ( optionname );
+
+CREATE TABLE radius_usergroup (
+  usergroupnum int primary key,
+  svcnum int not null,
+  groupname varchar(80) not null
+);
+CREATE INDEX radius_usergroup1 ON radius_usergroup ( svcnum );
+CREATE INDEX radius_usergroup2 ON radius_usergroup ( groupname );
+
+CREATE TABLE msgcat (
+  msgnum int primary key,
+  msgcode varchar(80) not null,
+  locale varchar(16) not null,
+  msg text not null
+);
+CREATE INDEX msgcat1 ON msgcat ( msgcode, locale );
+
+CREATE TABLE cust_tax_exempt (
+  exemptnum int primary key,
+  custnum int not null,
+  taxnum int not null,
+  year int not null,
+  month int not null,
+  amount decimal(10,2)
+);
+CREATE UNIQUE INDEX cust_tax_exempt1 ON cust_tax_exempt ( taxnum, year, month );
+
+ALTER TABLE svc_acct ADD domsvc integer NULL;
+ALTER TABLE part_svc ADD svc_acct__domsvc varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_acct__domsvc_flag char(1) NULL;
+ALTER TABLE svc_domain ADD catchall integer NULL;
+ALTER TABLE cust_main ADD referral_custnum integer NULL;
+ALTER TABLE cust_main ADD comments text NULL;
+ALTER TABLE cust_pay ADD custnum integer;
+ALTER TABLE cust_pay_batch ADD paybatchnum integer;
+ALTER TABLE cust_refund ADD custnum integer;
+ALTER TABLE cust_pkg ADD manual_flag char(1) NULL;
+ALTER TABLE part_pkg ADD plan varchar(80) NULL;
+ALTER TABLE part_pkg ADD plandata text NULL;
+ALTER TABLE part_pkg ADD setuptax char(1) NULL;
+ALTER TABLE part_pkg ADD recurtax char(1) NULL;
+ALTER TABLE part_pkg ADD disabled char(1) NULL;
+ALTER TABLE part_svc ADD disabled char(1) NULL;
+ALTER TABLE cust_bill ADD closed char(1) NULL;
+ALTER TABLE cust_pay ADD closed char(1) NULL;
+ALTER TABLE cust_credit ADD closed char(1) NULL;
+ALTER TABLE cust_refund ADD closed char(1) NULL;
+ALTER TABLE cust_bill_event ADD status varchar(80);
+ALTER TABLE cust_bill_event ADD statustext text NULL;
+ALTER TABLE svc_acct ADD sec_phrase varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_acct__sec_phrase varchar(80) NULL;
+ALTER TABLE part_svc ADD svc_acct__sec_phrase_flag char(1) NULL;
+ALTER TABLE part_pkg ADD taxclass varchar(80) NULL;
+ALTER TABLE cust_main_county ADD taxclass varchar(80) NULL;
+ALTER TABLE cust_main_county ADD exempt_amount decimal(10,2);
+CREATE INDEX cust_main3 ON cust_main ( referral_custnum );
+CREATE INDEX cust_credit_bill1 ON cust_credit_bill ( crednum );
+CREATE INDEX cust_credit_bill2 ON cust_credit_bill ( invnum );
+CREATE INDEX cust_bill_pay1 ON cust_bill_pay ( invnum );
+CREATE INDEX cust_bill_pay2 ON cust_bill_pay ( paynum );
+CREATE INDEX cust_credit_refund1 ON cust_credit_refund ( crednum );
+CREATE INDEX cust_credit_refund2 ON cust_credit_refund ( refundnum );
+CREATE UNIQUE INDEX cust_pay_batch_pkey ON cust_pay_batch ( paybatchnum );
+CREATE UNIQUE INDEX part_svc_column1 ON part_svc_column ( svcpart, columnname );
+CREATE INDEX cust_pay2 ON cust_pay ( paynum );
+CREATE INDEX cust_pay3 ON cust_pay ( custnum );
+CREATE INDEX cust_pay4 ON cust_pay ( paybatch );
+</pre>
+
+  <li>If you are using PostgreSQL, apply the following changes to your database:
+<pre>
+CREATE UNIQUE INDEX agent_pkey ON agent ( agentnum );
+CREATE UNIQUE INDEX agent_type_pkey ON agent_type ( typenum );
+CREATE UNIQUE INDEX cust_bill_pkey ON cust_bill ( invnum );
+CREATE UNIQUE INDEX cust_credit_pkey ON cust_credit ( crednum );
+CREATE UNIQUE INDEX cust_main_pkey ON cust_main ( custnum );
+CREATE UNIQUE INDEX cust_main_county_pkey ON cust_main_county ( taxnum );
+CREATE UNIQUE INDEX cust_main_invoice_pkey ON cust_main_invoice ( destnum );
+CREATE UNIQUE INDEX cust_pay_pkey ON cust_pay ( paynum );
+CREATE UNIQUE INDEX cust_pkg_pkey ON cust_pkg ( pkgnum );
+CREATE UNIQUE INDEX cust_refund_pkey ON cust_refund ( refundnum );
+CREATE UNIQUE INDEX cust_svc_pkey ON cust_svc ( svcnum );
+CREATE UNIQUE INDEX domain_record_pkey ON domain_record ( recnum );
+CREATE UNIQUE INDEX nas_pkey ON nas ( nasnum );
+CREATE UNIQUE INDEX part_pkg_pkey ON part_pkg ( pkgpart );
+CREATE UNIQUE INDEX part_referral_pkey ON part_referral ( refnum );
+CREATE UNIQUE INDEX part_svc_pkey ON part_svc ( svcpart );
+CREATE UNIQUE INDEX port_pkey ON port ( portnum );
+CREATE UNIQUE INDEX prepay_credit_pkey ON prepay_credit ( prepaynum );
+CREATE UNIQUE INDEX session_pkey ON session ( sessionnum );
+CREATE UNIQUE INDEX svc_acct_pkey ON svc_acct ( svcnum );
+CREATE UNIQUE INDEX svc_acct_pop_pkey ON svc_acct_pop ( popnum );
+CREATE UNIQUE INDEX svc_acct_sm_pkey ON svc_acct_sm ( svcnum );
+CREATE UNIQUE INDEX svc_domain_pkey ON svc_domain ( svcnum );
+CREATE UNIQUE INDEX svc_www_pkey ON svc_www ( svcnum );
+</pre>
+  <li>If you wish to enable service/shipping addresses, apply the following
+      changes to your database:
+<pre>
+ALTER TABLE cust_main ADD COLUMN ship_last varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_first varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_company varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_address1 varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_address2 varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_city varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_county varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_state varchar(80) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_zip varchar(10) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_country char(2) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_daytime varchar(20) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_night varchar(20) NULL;
+ALTER TABLE cust_main ADD COLUMN ship_fax varchar(12) NULL;
+CREATE INDEX cust_main4 ON cust_main ( ship_last );
+CREATE INDEX cust_main5 ON cust_main ( ship_company );
+</pre>
+  <li>If you are using the signup server, reinstall it according to the <a href="signup.html">instructions</a>.  The 1.3.x signup server is not compatible with 1.4.x.
+  <li>Run <tt>bin/dbdef-create <i>username</i></tt>
+  <li>If you have svc_acct_sm records or service definitions:
+    <ul>
+      <li>Create a service definition with table svc_forward
+      <li>Run <tt>bin/fs-migrate-svc_acct_sm <i>username</i></tt>
+    </ul>
+  <li>Or if you just have svc_acct records:
+    <ul>
+      <li>Order and provision a package for your default domain and note down the <b>Service #</b> or <i>svcnum</i>.
+      <li><tt>UPDATE svc_acct SET domsvc = </tt><i>svcnum</i>
+      <li>Update your service definitions to have default (or fixed) <b>domsvc</b>.
+    </ul>
+  <li>Run <tt>bin/fs-migrate-payref<i>username</i></tt>
+  <li>Run <tt>bin/fs-migrate-part_svc<i>username</i></tt>
+  <li><b>After running bin/fs-migrate-payref</b>, apply the following changes to your database:
+  <table border><tr><th>PostgreSQL</th><th>MySQL, others</th></tr>
+<tr><td>
+<font size=-1><pre>
+CREATE TABLE cust_pay_temp (
+  paynum int primary key,
+  custnum int not null,
+  paid decimal(10,2) not null,
+  _date int null,
+  payby char(4) not null,
+  payinfo varchar(16) null,
+  paybatch varchar(80) null,
+  closed char(1) null
+);
+INSERT INTO cust_pay_temp SELECT paynum, custnum, paid, _date, payby, payinfo, paybatch, closed FROM cust_pay;
+DROP TABLE cust_pay;
+ALTER TABLE cust_pay_temp RENAME TO cust_pay;
+CREATE UNIQUE INDEX cust_pay1 ON cust_pay (paynum);
+CREATE TABLE cust_refund_temp (
+  refundnum int primary key,
+  custnum int not null,
+  _date int null,
+  refund decimal(10,2) not null,
+  otaker varchar(8) not null,
+  reason varchar(80) not null,
+  payby char(4) not null,
+  payinfo varchar(16) null,
+  paybatch varchar(80) null,
+  closed char(1) null
+);
+INSERT INTO cust_refund_temp SELECT refundnum, custnum, _date, refund, otaker, reason, payby, payinfo, '', closed from cust_refund;
+DROP TABLE cust_refund;
+ALTER TABLE cust_refund_temp RENAME TO cust_refund;
+CREATE UNIQUE INDEX cust_refund1 ON cust_refund (refundnum);
+</pre></font>
+</td><td>
+<font size=-1><pre>
+ALTER TABLE cust_pay DROP COLUMN invnum;
+ALTER TABLE cust_refund DROP COLUMN crednum;
+</pre></font>
+</td></tr></table>
+  <li><b>IMPORTANT: After applying the second set of database changes</b>, run <tt>bin/dbdef-create <i>username</i></tt> again.
+  <li><b>IMPORTANT</b>: run <tt>bin/create-history-tables <i>username</i></tt>
+  <li><b>IMPORTANT: After running bin/create-history-tables</b>, run <tt>bin/dbdef-create <i>username</i></tt> again.
+  <li>As the freeside UNIX user, run <tt>bin/populate-msgcat <i>username</i></tt
+> to populate the message catalog
+<!--  <li>set the <a href="../config/config.cgi#username_policy">user_policy configuration value</a> as appropriate for your site. -->
+  <li>set the <a href="../config/config.cgi#locale">locale configuration value</a> to en_US.
+  <li>the mxmachines, nsmachines, arecords and cnamerecords configuration values have been deprecated.  Set the <a href="../config/config.cgi#defaultrecords">defaultrecords configuration value</a> instead.
+  <li>Create the `/usr/local/etc/freeside/cache.<i>datasrc</i>' directory
+      (owned by the freeside user).
+  <li>freeside-queued was installed with the Perl modules.  Start it now and ensure that is run upon system startup.
+  <li>Set appropriate <a href="../browse/part_bill_event.cgi">invoice events</a> for your site.  At the very least, you'll want to set some invoice events "<i>After 0 days</i>": a <i>BILL</i> invoice event to print invoices, a <i>CARD</i> invoice event to batch or run cards real-time, and a <i>COMP</i> invoice event to "pay" complimentary customers.  If you were using the <i>-i</i> option to <a href="man/bin/freeside-bill.html">freeside-bill</a> it should be removed.
+  <li>Use <a href="man/bin/freeside-daily.html">freeside-daily</a> instead of <a href="man/bin/freeside-bill.html">freeside-bill</a>.
+  <li>If you would like Freeside to notify your customers when their credit
+  cards and other billing arrangements are about to expire, arrange for
+  <b>freeside-expiration-alerter</b> to be run daily by cron or similar
+  facility.  The message it sends can be configured from the
+  <u>Configuration</u> choice of the main menu as <u>alerter_template</u>.
+  <li>Export has been rewritten.  If you were using the icradiusmachines,
+  icradius_mysqldest, icradius_mysqlsource, or icradius_secrets files, add
+  an appropriate "sqlradius" export to all relevant Service Definitions
+  instead.  Use <a href="http://www.mysql.com/documentation/mysql/bychapter/manual_MySQL_Database_Administration.html#Replication">MySQL replication</a> or
+  point the "sqlradius" export directly at your external ICRADIUS or FreeRADIUS
+  database (or through an SSL-necrypting proxy...)
+</ul>
+</body>
diff --git a/httemplate/docs/upgrade9.html b/httemplate/docs/upgrade9.html
new file mode 100644 (file)
index 0000000..24d1cce
--- /dev/null
@@ -0,0 +1,26 @@
+<head>
+  <title>Upgrading to 1.4.1</title>
+</head>
+<body>
+<h1>Upgrading to 1.4.1 from 1.4.0</h1>
+<ul>
+  <li>If migrating from less than 1.4.0, see these <a href="upgrade8.html">instructions</a> first.
+  <li>Back up your data and current Freeside installation.
+  <li>Run <code>make aspdocs</code> or <code>make masondocs</code>.
+  <li>Copy <code>aspdocs/</code> or <code>masondocs/</code> to your web server's document space.
+  <li>Run <code>make install-perl-modules</code>.
+  <li>Install <a href="http://search.cpan.org/search?dist=Net-SSH">Net::SSH</a> minimum version 0.07
+  <li>Apply the following changes to your database:
+<pre>
+INSERT INTO msgcat ( msgnum, msgcode, locale, msg ) VALUES ( 18, 'daytime', 'en_US', 'Day Phone' );
+INSERT INTO msgcat ( msgnum, msgcode, locale, msg ) VALUES ( 19, 'night', 'en_US', 'Night Phone' );
+</pre>
+  <li>Optionally, apply the following changes to your database (performance improvements):
+<pre>
+CREATE INDEX part_pkg1 ON part_pkg ( disabled );
+CREATE INDEX part_svc1 ON part_svc ( disabled );
+CREATE INDEX cust_bill2 ON cust_bill ( _date );
+</pre>
+  <li>If you want to use ACH (electronic checks), you will need to make changes to your database.  The easiest way to make these changes is to dump your database (with pg_dump), change the payinfo field in the cust_pay, cust_refund, h_cust_pay and h_cust_refund tables from varchar(16) to varchar(80), reload the database from the dump, and run dbdef-create
+  <li>Restart Apache and freeside-queued.
+</body>
diff --git a/httemplate/edit/REAL_cust_pkg.cgi b/httemplate/edit/REAL_cust_pkg.cgi
new file mode 100755 (executable)
index 0000000..e44acba
--- /dev/null
@@ -0,0 +1,97 @@
+<!-- mason kludge -->
+<%
+# <!-- $Id: REAL_cust_pkg.cgi,v 1.5 2003-04-01 01:22:24 ivan Exp $ -->
+
+my $error ='';
+my $pkgnum = '';
+if ( $cgi->param('error') ) {
+  $error = $cgi->param('error');
+  $pkgnum = $cgi->param('pkgnum');
+} else {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/ or die "no pkgnum";
+  $pkgnum = $1;
+}
+
+#get package record
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+die "No package!" unless $cust_pkg;
+my $part_pkg = qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->getfield('pkgpart')});
+
+if ( $error ) {
+  #$cust_pkg->$_(str2time($cgi->param($_)) foreach qw(setup bill);
+  $cust_pkg->setup(str2time($cgi->param('setup')));
+  $cust_pkg->bill(str2time($cgi->param('bill')));
+}
+
+#my $custnum = $cust_pkg->getfield('custnum');
+print header('Package Edit'); #, menubar(
+#  "View this customer (#$custnum)" => popurl(2). "view/cust_main.cgi?$custnum",
+#  'Main Menu' => popurl(2)
+#));
+
+#print info
+my($susp,$cancel,$expire)=(
+  $cust_pkg->getfield('susp'),
+  $cust_pkg->getfield('cancel'),
+  $cust_pkg->getfield('expire'),
+);
+my($pkg,$comment)=($part_pkg->getfield('pkg'),$part_pkg->getfield('comment'));
+my($setup,$bill)=($cust_pkg->getfield('setup'),$cust_pkg->getfield('bill'));
+my $otaker = $cust_pkg->getfield('otaker');
+
+print '<FORM NAME="formname" ACTION="process/REAL_cust_pkg.cgi" METHOD="POST">',      qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!;
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: $error</FONT>!
+  if $error;
+
+print ntable("#cccccc",2),
+      '<TR><TD ALIGN="right">Package number</TD><TD BGCOLOR="#ffffff">',
+      $pkgnum, '</TD></TR>',
+      '<TR><TD ALIGN="right">Package</TD><TD BGCOLOR="#ffffff">',
+      $pkg,  '</TD></TR>',
+      '<TR><TD ALIGN="right">Comment</TD><TD BGCOLOR="#ffffff">',
+      $comment,  '</TD></TR>',
+      '<TR><TD ALIGN="right">Order taker</TD><TD BGCOLOR="#ffffff">',
+      $otaker,  '</TD></TR>',
+      '<TR><TD ALIGN="right">Setup date</TD><TD>'.
+      '<INPUT TYPE="text" NAME="setup" SIZE=32 VALUE="',
+      ( $setup ? time2str("%c %z (%Z)",$setup) : "" ), '"></TD></TR>';
+
+print '<TR><TD ALIGN="right">Last bill date</TD><TD>',
+      '<INPUT TYPE="text" NAME="last_bill" SIZE=32 VALUE="',
+      ( $cust_pkg->last_bill
+        ? time2str("%c %z (%Z)", $cust_pkg->last_bill)
+        : ""                                          ),
+      '"></TD></TR>'
+  if $cust_pkg->dbdef_table->column('last_bill');
+
+print '<TR><TD ALIGN="right">Next bill date</TD><TD>',
+      '<INPUT TYPE="text" NAME="bill" SIZE=32 VALUE="',
+      ( $bill ? time2str("%c %z (%Z)",$bill) : "" ), '"></TD></TR>';
+
+print '<TR><TD ALIGN="right">Suspension date</TD><TD BGCOLOR="#ffffff">',
+       time2str("%D",$susp), '</TD></TR>'
+  if $susp;
+
+#print '<TR><TD ALIGN="right">Expiration date</TD><TD BGCOLOR="#ffffff">',
+#       time2str("%D",$expire), '</TD></TR>'
+#  if $expire;
+print '<TR><TD ALIGN="right">Expiration date'.
+      '</TD><TD>',
+      '<INPUT TYPE="text" NAME="expire" SIZE=32 VALUE="',
+      ( $expire ? time2str("%c %z (%Z)",$expire) : "" ), '">'.
+      '<BR><FONT SIZE=-1>(will <b>cancel</b> this package'.
+      ' when the date is reached)</FONT>'.
+      '</TD></TR>';
+
+print '<TR><TD ALIGN="right">Cancellation date</TD><TD BGCOLOR="#ffffff">',
+       time2str("%D",$cancel), '</TD></TR>'
+  if $cancel;
+
+%>
+</TABLE>
+<BR><INPUT TYPE="submit" VALUE="Apply Changes">
+</FORM>
+</BODY>
+</HTML>
diff --git a/httemplate/edit/agent.cgi b/httemplate/edit/agent.cgi
new file mode 100755 (executable)
index 0000000..449456c
--- /dev/null
@@ -0,0 +1,74 @@
+<!-- mason kludge -->
+<%
+
+my $agent;
+if ( $cgi->param('error') ) {
+  $agent = new FS::agent ( {
+    map { $_, scalar($cgi->param($_)) } fields('agent')
+  } );
+} elsif ( $cgi->keywords ) {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $agent = qsearchs( 'agent', { 'agentnum' => $1 } );
+} else { #adding
+  $agent = new FS::agent {};
+}
+my $action = $agent->agentnum ? 'Edit' : 'Add';
+my $hashref = $agent->hashref;
+
+print header("$action Agent", menubar(
+  'Main Menu' => $p,
+  'View all agents' => $p. 'browse/agent.cgi',
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print '<FORM ACTION="', popurl(1), 'process/agent.cgi" METHOD=POST>',
+      qq!<INPUT TYPE="hidden" NAME="agentnum" VALUE="$hashref->{agentnum}">!,
+      "Agent #", $hashref->{agentnum} ? $hashref->{agentnum} : "(NEW)";
+
+print &ntable("#cccccc", 2, ''), <<END;
+<TR>
+  <TH ALIGN="right">Agent</TH>
+  <TD><INPUT TYPE="text" NAME="agent" SIZE=32 VALUE="$hashref->{agent}"></TD>
+</TR>
+<TR>
+  <TH ALIGN="right">Agent type</TH>
+  <TD><SELECT NAME="typenum" SIZE=1>
+END
+
+foreach my $agent_type (qsearch('agent_type',{})) {
+  print "<OPTION VALUE=". $agent_type->typenum;
+  print " SELECTED"
+    if $hashref->{typenum} && ( $hashref->{typenum} == $agent_type->typenum );
+  print ">", $agent_type->getfield('typenum'), ": ",
+        $agent_type->getfield('atype'),"\n";
+}
+
+print <<END;
+</SELECT></TD>
+</TR>
+<TR>
+  <TD ALIGN="right"><!--Frequency--></TD>
+  <TD><INPUT TYPE="hidden" NAME="freq" VALUE="$hashref->{freq}"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right"><!--Program--></TD>
+  <TD><INPUT TYPE="hidden" NAME="prog" VALUE="$hashref->{prog}"></TD>
+</TR>
+</TABLE>
+END
+
+print qq!<BR><INPUT TYPE="submit" VALUE="!,
+      $hashref->{agentnum} ? "Apply changes" : "Add agent",
+      qq!">!;
+
+print <<END;
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/agent_type.cgi b/httemplate/edit/agent_type.cgi
new file mode 100755 (executable)
index 0000000..637c710
--- /dev/null
@@ -0,0 +1,63 @@
+<!-- mason kludge -->
+<%
+
+my($agent_type);
+if ( $cgi->param('error') ) {
+  $agent_type = new FS::agent_type ( {
+    map { $_, scalar($cgi->param($_)) } fields('agent')
+  } );
+} elsif ( $cgi->keywords ) { #editing
+  my( $query ) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $agent_type=qsearchs('agent_type',{'typenum'=>$1});
+} else { #adding
+  $agent_type = new FS::agent_type {};
+}
+my $action = $agent_type->typenum ? 'Edit' : 'Add';
+my $hashref = $agent_type->hashref;
+
+print header("$action Agent Type", menubar(
+  'Main Menu' => "$p",
+  'View all agent types' => "${p}browse/agent_type.cgi",
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print '<FORM ACTION="', popurl(1), 'process/agent_type.cgi" METHOD=POST>',
+      qq!<INPUT TYPE="hidden" NAME="typenum" VALUE="$hashref->{typenum}">!,
+      "Agent Type #", $hashref->{typenum} ? $hashref->{typenum} : "(NEW)";
+
+print <<END;
+<BR><BR>Agent Type <INPUT TYPE="text" NAME="atype" SIZE=32 VALUE="$hashref->{atype}">
+<BR><BR>Select which packages agents of this type may sell to customers<BR>
+END
+
+foreach my $part_pkg ( qsearch('part_pkg',{ 'disabled' => '' }) ) {
+  print qq!<BR><INPUT TYPE="checkbox" NAME="pkgpart!,
+        $part_pkg->getfield('pkgpart'), qq!" !,
+       # ( 'CHECKED 'x scalar(
+        qsearchs('type_pkgs',{
+          'typenum' => $agent_type->getfield('typenum'),
+          'pkgpart'  => $part_pkg->getfield('pkgpart'),
+        })
+          ? 'CHECKED '
+          : '',
+        qq!VALUE="ON"> !,
+    qq!<A HREF="${p}edit/part_pkg.cgi?!, $part_pkg->pkgpart, 
+    '">', $part_pkg->pkgpart. ": ". $part_pkg->getfield('pkg'), '</A>',
+  ;
+}
+
+print qq!<BR><BR><INPUT TYPE="submit" VALUE="!,
+      $hashref->{typenum} ? "Apply changes" : "Add agent type",
+      qq!">!;
+
+print <<END;
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_bill_pay.cgi b/httemplate/edit/cust_bill_pay.cgi
new file mode 100755 (executable)
index 0000000..8cdf450
--- /dev/null
@@ -0,0 +1,95 @@
+<!-- mason kludge -->
+<%
+
+my($paynum, $amount, $invnum);
+if ( $cgi->param('error') ) {
+  $paynum = $cgi->param('paynum');
+  $amount = $cgi->param('amount');
+  $invnum = $cgi->param('invnum');
+} else {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $paynum = $1;
+  $amount = '';
+  $invnum = '';
+}
+
+my $otaker = getotaker;
+
+my $p1 = popurl(1);
+
+print header("Apply Payment", '');
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT><BR><BR>"
+  if $cgi->param('error');
+print <<END;
+    <FORM ACTION="${p1}process/cust_bill_pay.cgi" METHOD=POST>
+END
+
+my $cust_pay = qsearchs('cust_pay', { 'paynum' => $paynum } );
+die "payment $paynum not found!" unless $cust_pay;
+
+my $unapplied = $cust_pay->unapplied;
+
+print "Payment # <B>$paynum</B>".
+      qq!<INPUT TYPE="hidden" NAME="paynum" VALUE="$paynum">!.
+      '<BR>Date: <B>'. time2str("%D", $cust_pay->_date). '</B>'.
+      '<BR>Amount: $<B>'. $cust_pay->paid. '</B>'.
+      "<BR>Unapplied amount: \$<B>$unapplied</B>"
+      ;
+
+my @cust_bill = grep $_->owed != 0,
+                qsearch('cust_bill', { 'custnum' => $cust_pay->custnum } );
+
+print <<END;
+<SCRIPT>
+function changed(what) {
+  cust_bill = what.options[what.selectedIndex].value;
+END
+
+foreach my $cust_bill ( @cust_bill ) {
+  my $invnum = $cust_bill->invnum;
+  my $changeto = $cust_bill->owed < $unapplied
+                   ? $cust_bill->owed 
+                   : $unapplied;
+  print <<END;
+  if ( cust_bill == $invnum ) {
+    what.form.amount.value = "$changeto";
+  }
+END
+}
+
+#  if ( cust_bill == "Refund" ) {
+#    what.form.amount.value = "$credited";
+#  }
+print <<END;
+}
+</SCRIPT>
+END
+
+print qq!<BR>Invoice #<SELECT NAME="invnum" SIZE=1 onChange="changed(this)">!,
+      '<OPTION VALUE="">';
+foreach my $cust_bill ( @cust_bill ) {
+  print '<OPTION'. ( $cust_bill->invnum eq $invnum ? ' SELECTED' : '' ).
+        ' VALUE="'. $cust_bill->invnum. '">'. $cust_bill->invnum.
+        ' -  '. time2str("%D",$cust_bill->_date).
+        ' - $'. $cust_bill->owed;
+}
+#print qq!<OPTION VALUE="Refund">Refund!;
+print "</SELECT>";
+
+print qq!<BR>Amount \$<INPUT TYPE="text" NAME="amount" VALUE="$amount" SIZE=8 MAXLENGTH=8>!;
+
+print <<END;
+<BR>
+<INPUT TYPE="submit" VALUE="Apply">
+END
+
+print <<END;
+
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_credit.cgi b/httemplate/edit/cust_credit.cgi
new file mode 100755 (executable)
index 0000000..aae0df2
--- /dev/null
@@ -0,0 +1,63 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+my($custnum, $amount, $reason);
+if ( $cgi->param('error') ) {
+  #$cust_credit = new FS::cust_credit ( {
+  #  map { $_, scalar($cgi->param($_)) } fields('cust_credit')
+  #} );
+  $custnum = $cgi->param('custnum');
+  $amount = $cgi->param('amount');
+  #$refund = $cgi->param('refund');
+  $reason = $cgi->param('reason');
+} else {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $custnum = $1;
+  $amount = '';
+  #$refund = 'yes';
+  $reason = '';
+}
+my $_date = time;
+
+my $otaker = getotaker;
+
+my $p1 = popurl(1);
+
+print header("Post Credit", '');
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+print <<END, small_custview($custnum, $conf->config('countrydefault'));
+    <FORM ACTION="${p1}process/cust_credit.cgi" METHOD=POST>
+    <INPUT TYPE="hidden" NAME="crednum" VALUE="">
+    <INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">
+    <INPUT TYPE="hidden" NAME="paybatch" VALUE="">
+    <INPUT TYPE="hidden" NAME="_date" VALUE="$_date">
+    <INPUT TYPE="hidden" NAME="credited" VALUE="">
+    <INPUT TYPE="hidden" NAME="otaker" VALUE="$otaker">
+END
+
+print '<BR><BR>Credit'. ntable("#cccccc", 2).
+      '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'.
+      time2str("%D",$_date).  '</TD></TR>';
+
+print qq!<TR><TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">\$<INPUT TYPE="text" NAME="amount" VALUE="$amount" SIZE=8 MAXLENGTH=8></TD></TR>!;
+
+#print qq! <INPUT TYPE="checkbox" NAME="refund" VALUE="$refund">Also post refund!;
+
+print qq!<TR><TD ALIGN="right">Reason</TD><TD BGCOLOR="#ffffff"><INPUT TYPE="text" NAME="reason" VALUE="$reason"></TD></TR>!;
+
+print qq!<TR><TD ALIGN="right">Auto-apply<BR>to invoices</TD><TD><SELECT NAME="apply"><OPTION VALUE="yes" SELECTED>yes<OPTION>no</SELECT></TD>!;
+
+print <<END;
+</TABLE>
+<BR>
+<INPUT TYPE="submit" VALUE="Post credit">
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_credit_bill.cgi b/httemplate/edit/cust_credit_bill.cgi
new file mode 100755 (executable)
index 0000000..1a97e13
--- /dev/null
@@ -0,0 +1,101 @@
+<!-- mason kludge -->
+<%
+
+my($crednum, $amount, $invnum);
+if ( $cgi->param('error') ) {
+  #$cust_credit_bill = new FS::cust_credit_bill ( {
+  #  map { $_, scalar($cgi->param($_)) } fields('cust_credit_bill')
+  #} );
+  $crednum = $cgi->param('crednum');
+  $amount = $cgi->param('amount');
+  #$refund = $cgi->param('refund');
+  $invnum = $cgi->param('invnum');
+} else {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $crednum = $1;
+  $amount = '';
+  #$refund = 'yes';
+  $invnum = '';
+}
+
+my $otaker = getotaker;
+
+my $p1 = popurl(1);
+
+print header("Apply Credit", '');
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT><BR><BR>"
+  if $cgi->param('error');
+print <<END;
+    <FORM ACTION="${p1}process/cust_credit_bill.cgi" METHOD=POST>
+END
+
+my $cust_credit = qsearchs('cust_credit', { 'crednum' => $crednum } );
+die "credit $crednum not found!" unless $cust_credit;
+
+my $credited = $cust_credit->credited;
+
+print "Credit # <B>$crednum</B>".
+      qq!<INPUT TYPE="hidden" NAME="crednum" VALUE="$crednum">!.
+      '<BR>Date: <B>'. time2str("%D", $cust_credit->_date). '</B>'.
+      '<BR>Amount: $<B>'. $cust_credit->amount. '</B>'.
+      "<BR>Unapplied amount: \$<B>$credited</B>".
+      '<BR>Reason: <B>'. $cust_credit->reason. '</B>'
+      ;
+
+my @cust_bill = grep $_->owed != 0,
+                qsearch('cust_bill', { 'custnum' => $cust_credit->custnum } );
+
+print <<END;
+<SCRIPT>
+function changed(what) {
+  cust_bill = what.options[what.selectedIndex].value;
+END
+
+foreach my $cust_bill ( @cust_bill ) {
+  my $invnum = $cust_bill->invnum;
+  my $changeto = $cust_bill->owed < $cust_credit->credited
+                   ? $cust_bill->owed 
+                   : $cust_credit->credited;
+  print <<END;
+  if ( cust_bill == $invnum ) {
+    what.form.amount.value = "$changeto";
+  }
+END
+}
+
+print <<END;
+  if ( cust_bill == "Refund" ) {
+    what.form.amount.value = "$credited";
+  }
+}
+</SCRIPT>
+END
+
+print qq!<BR>Invoice #<SELECT NAME="invnum" SIZE=1 onChange="changed(this)">!,
+      '<OPTION VALUE="">';
+foreach my $cust_bill ( @cust_bill ) {
+  print '<OPTION'. ( $cust_bill->invnum eq $invnum ? ' SELECTED' : '' ).
+        ' VALUE="'. $cust_bill->invnum. '">'. $cust_bill->invnum.
+        ' -  '. time2str("%D",$cust_bill->_date).
+        ' - $'. $cust_bill->owed;
+}
+print qq!<OPTION VALUE="Refund">Refund!;
+print "</SELECT>";
+
+print qq!<BR>Amount \$<INPUT TYPE="text" NAME="amount" VALUE="$amount" SIZE=8 MAXLENGTH=8>!;
+
+print <<END;
+<BR>
+<INPUT TYPE="submit" VALUE="Apply">
+END
+
+print <<END;
+
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi
new file mode 100755 (executable)
index 0000000..2b7d8d0
--- /dev/null
@@ -0,0 +1,523 @@
+<!-- mason kludge -->
+<%
+
+  #for misplaced logic below
+  #use FS::part_pkg;
+
+  #for false laziness below (now more properly lazy)
+  #use FS::svc_acct_pop;
+
+  #for (other) false laziness below
+  #use FS::agent;
+  #use FS::type_pkgs;
+
+my $conf = new FS::Conf;
+
+#get record
+
+my $error = '';
+my($custnum, $username, $password, $popnum, $cust_main, $saved_pkgpart);
+my(@invoicing_list);
+if ( $cgi->param('error') ) {
+  $error = $cgi->param('error');
+  $cust_main = new FS::cust_main ( {
+    map { $_, scalar($cgi->param($_)) } fields('cust_main')
+  } );
+  $custnum = $cust_main->custnum;
+  $saved_pkgpart = $cgi->param('pkgpart_svcpart') || '';
+  if ( $saved_pkgpart =~ /^(\d+)_/ ) {
+    $saved_pkgpart = $1;
+  } else {
+    $saved_pkgpart = '';
+  }
+  $username = $cgi->param('username');
+  $password = $cgi->param('_password');
+  $popnum = $cgi->param('popnum');
+  @invoicing_list = split( /\s*,\s*/, $cgi->param('invoicing_list') );
+} elsif ( $cgi->keywords ) { #editing
+  my( $query ) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $custnum=$1;
+  $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+  $saved_pkgpart = 0;
+  $username = '';
+  $password = '';
+  $popnum = 0;
+  @invoicing_list = $cust_main->invoicing_list;
+} else {
+  $custnum='';
+  $cust_main = new FS::cust_main ( {} );
+  $cust_main->otaker( &getotaker );
+  $cust_main->referral_custnum( $cgi->param('referral_custnum') );
+  $saved_pkgpart = 0;
+  $username = '';
+  $password = '';
+  $popnum = 0;
+  @invoicing_list = ();
+}
+$cgi->delete_all();
+my $action = $custnum ? 'Edit' : 'Add';
+
+# top
+
+my $p1 = popurl(1);
+print header("Customer $action", '');
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $error, "</FONT>"
+  if $error;
+
+print qq!<FORM ACTION="${p1}process/cust_main.cgi" METHOD=POST NAME="form1">!,
+      qq!<INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!,
+      qq!Customer # !, ( $custnum ? "<B>$custnum</B>" : " (NEW)" ),
+      
+;
+
+# agent
+
+my $r = qq!<font color="#ff0000">*</font>&nbsp;!;
+
+my @agents = qsearch( 'agent', {} );
+#die "No agents created!" unless @agents;
+eidiot "You have not created any agents.  You must create at least one agent before adding a customer.  Go to ". popurl(2). "browse/agent.cgi and create one or more agents." unless @agents;
+my $agentnum = $cust_main->agentnum || $agents[0]->agentnum; #default to first
+if ( scalar(@agents) == 1 ) {
+  print qq!<INPUT TYPE="hidden" NAME="agentnum" VALUE="$agentnum">!;
+} else {
+  print qq!<BR><BR>${r}Agent <SELECT NAME="agentnum" SIZE="1">!;
+  my $agent;
+  foreach $agent (sort {
+    $a->agent cmp $b->agent;
+  } @agents) {
+      print '<OPTION VALUE="', $agent->agentnum, '"',
+      " SELECTED"x($agent->agentnum==$agentnum),
+      ">". $agent->agent;
+      #">", $agent->agentnum,": ", $agent->agent;
+  }
+  print "</SELECT>";
+}
+
+#referral
+
+my $refnum = $cust_main->refnum || $conf->config('referraldefault') || 0;
+if ( $custnum && ! $conf->exists('editreferrals') ) {
+  print qq!<INPUT TYPE="hidden" NAME="refnum" VALUE="$refnum">!;
+} else {
+  my(@referrals) = qsearch('part_referral',{});
+  if ( scalar(@referrals) == 0 ) {
+    eidiot "You have not created any advertising sources.  You must create at least one advertising source before adding a customer.  Go to ". popurl(2). "browse/part_referral.cgi and create one or more advertising sources.";
+  } elsif ( scalar(@referrals) == 1 ) {
+    $refnum ||= $referrals[0]->refnum;
+    print qq!<INPUT TYPE="hidden" NAME="refnum" VALUE="$refnum">!;
+  } else {
+    print qq!<BR><BR>${r}Advertising source <SELECT NAME="refnum" SIZE="1">!;
+    print "<OPTION> " unless $refnum;
+    my($referral);
+    foreach $referral (sort {
+      $a->refnum <=> $b->refnum;
+    } @referrals) {
+      print "<OPTION" . " SELECTED"x($referral->refnum==$refnum),
+      ">", $referral->refnum, ": ", $referral->referral;
+    }
+    print "</SELECT>";
+  }
+}
+
+#referring customer
+
+#print qq!<BR><BR>Referring Customer: !;
+my $referring_cust_main = '';
+if ( $cust_main->referral_custnum
+     and $referring_cust_main =
+           qsearchs('cust_main', { custnum => $cust_main->referral_custnum } )
+) {
+  print '<BR><BR>Referring Customer: <A HREF="'. popurl(1). '/cust_main.cgi?'.
+        $cust_main->referral_custnum. '">'.
+        $cust_main->referral_custnum. ': '.
+        ( $referring_cust_main->company
+          || $referring_cust_main->last. ', '. $referring_cust_main->first ).
+        '</A><INPUT TYPE="hidden" NAME="referral_custnum" VALUE="'.
+        $cust_main->referral_custnum. '">';
+} elsif ( ! $conf->exists('disable_customer_referrals') ) {
+  print '<BR><BR>Referring customer number: <INPUT TYPE="text" NAME="referral_custnum" VALUE="">';
+} else {
+  print '<INPUT TYPE="hidden" NAME="referral_custnum" VALUE="">';
+}
+
+# contact info
+
+my($last,$first,$ss,$company,$address1,$address2,$city,$zip)=(
+  $cust_main->last,
+  $cust_main->first,
+  $cust_main->ss,
+  $cust_main->company,
+  $cust_main->address1,
+  $cust_main->address2,
+  $cust_main->city,
+  $cust_main->zip,
+);
+
+print "<BR><BR>Billing address", &itable("#cccccc"), <<END;
+<TR><TH ALIGN="right">${r}Contact&nbsp;name<BR>(last,&nbsp;first)</TH><TD COLSPAN=3>
+END
+
+print <<END;
+<INPUT TYPE="text" NAME="last" VALUE="$last"> , 
+<INPUT TYPE="text" NAME="first" VALUE="$first">
+</TD>
+END
+
+if ( $conf->exists('show_ss') ) {
+  print qq!<TD ALIGN="right">SS#</TD><TD><INPUT TYPE="text" NAME="ss" VALUE="$ss" SIZE=11></TD>!;
+} else {
+  print qq!<TD><INPUT TYPE="hidden" NAME="ss" VALUE="$ss"></TD>!;
+}
+
+print <<END;
+</TR>
+<TR><TD ALIGN="right">Company</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="company" VALUE="$company" SIZE=70></TD></TR>
+<TR><TH ALIGN="right">${r}Address</TH><TD COLSPAN=5><INPUT TYPE="text" NAME="address1" VALUE="$address1" SIZE=70></TD></TR>
+<TR><TD ALIGN="right">&nbsp;</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="address2" VALUE="$address2" SIZE=70></TD></TR>
+<TR><TH ALIGN="right">${r}City</TH><TD><INPUT TYPE="text" NAME="city" VALUE="$city"></TD><TH ALIGN="right">${r}State</TH><TD>
+END
+
+#false laziness with ship state
+my $countrydefault = $conf->config('countrydefault') || 'US';
+$cust_main->country( $countrydefault ) unless $cust_main->country;
+
+$cust_main->state( $conf->config('statedefault') || 'CA' )
+  unless $cust_main->state || $cust_main->country ne 'US';
+
+my($county_html, $state_html, $country_html) =
+  FS::cust_main_county::regionselector( $cust_main->county,
+                                        $cust_main->state,
+                                        $cust_main->country );
+
+print "$county_html $state_html";
+
+print qq!</TD><TH>${r}Zip</TH><TD><INPUT TYPE="text" NAME="zip" VALUE="$zip" SIZE=10></TD></TR>!;
+
+my($daytime,$night,$fax)=(
+  $cust_main->daytime,
+  $cust_main->night,
+  $cust_main->fax,
+);
+
+my $daytime_label = FS::Msgcat::_gettext('daytime') || 'Day Phone';
+my $night_label = FS::Msgcat::_gettext('night') || 'Night Phone';
+
+print <<END;
+<TR><TH ALIGN="right">${r}Country</TH><TD>$country_html</TD></TR>
+<TR><TD ALIGN="right">$daytime_label</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="daytime" VALUE="$daytime" SIZE=18></TD></TR>
+<TR><TD ALIGN="right">$night_label</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="night" VALUE="$night" SIZE=18></TD></TR>
+<TR><TD ALIGN="right">Fax</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="fax" VALUE="$fax" SIZE=12></TD></TR>
+END
+
+print "</TABLE>${r}required fields<BR>";
+
+# service address
+
+if ( defined $cust_main->dbdef_table->column('ship_last') ) {
+
+  print "\n", <<END;
+  <SCRIPT>
+  function changed(what) {
+    what.form.same.checked = false;
+  }
+  function samechanged(what) {
+    if ( what.checked ) {
+END
+print "      what.form.ship_$_.value = what.form.$_.value;\n"
+  for (qw( last first company address1 address2 city zip daytime night fax ));
+print <<END;
+      what.form.ship_country.selectedIndex = what.form.country.selectedIndex;
+      ship_country_changed(what.form.ship_country);
+      what.form.ship_state.selectedIndex = what.form.state.selectedIndex;
+      ship_state_changed(what.form.ship_state);
+      what.form.ship_county.selectedIndex = what.form.county.selectedIndex;
+    }
+  }
+  </SCRIPT>
+END
+
+  print '<BR>Service address ',
+        '(<INPUT TYPE="checkbox" NAME="same" VALUE="Y" onClick="samechanged(this)"';
+  unless ( $cust_main->ship_last && $cgi->param('same') ne 'Y' ) {
+    print ' CHECKED';
+    foreach (
+      qw( last first company address1 address2 city county state zip country
+          daytime night fax )
+    ) {
+      $cust_main->set("ship_$_", $cust_main->get($_) );
+    }
+  }
+  print '>same as billing address)<BR>';
+
+  my($ship_last,$ship_first,$ship_company,$ship_address1,$ship_address2,$ship_city,$ship_zip)=(
+    $cust_main->ship_last,
+    $cust_main->ship_first,
+    $cust_main->ship_company,
+    $cust_main->ship_address1,
+    $cust_main->ship_address2,
+    $cust_main->ship_city,
+    $cust_main->ship_zip,
+  );
+
+  print &itable("#cccccc"), <<END;
+  <TR><TH ALIGN="right">${r}Contact&nbsp;name<BR>(last,&nbsp;first)</TH><TD COLSPAN=5>
+END
+
+  print <<END;
+  <INPUT TYPE="text" NAME="ship_last" VALUE="$ship_last" onChange="changed(this)"> , 
+  <INPUT TYPE="text" NAME="ship_first" VALUE="$ship_first" onChange="changed(this)">
+END
+
+  print <<END;
+  </TD></TR>
+  <TR><TD ALIGN="right">Company</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_company" VALUE="$ship_company" SIZE=70 onChange="changed(this)"></TD></TR>
+  <TR><TH ALIGN="right">${r}Address</TH><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_address1" VALUE="$ship_address1" SIZE=70 onChange="changed(this)"></TD></TR>
+  <TR><TD ALIGN="right">&nbsp;</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_address2" VALUE="$ship_address2" SIZE=70 onChange="changed(this)"></TD></TR>
+  <TR><TH ALIGN="right">${r}City</TH><TD><INPUT TYPE="text" NAME="ship_city" VALUE="$ship_city" onChange="changed(this)"></TD><TH ALIGN="right">${r}State</TH><TD>
+END
+
+  #false laziness with regular state
+  $cust_main->ship_country( $countrydefault ) unless $cust_main->ship_country;
+
+  $cust_main->ship_state( $conf->config('statedefault') || 'CA' )
+    unless $cust_main->ship_state || $cust_main->ship_country ne 'US';
+
+  my($ship_county_html, $ship_state_html, $ship_country_html) =
+    FS::cust_main_county::regionselector( $cust_main->ship_county,
+                                          $cust_main->ship_state,
+                                          $cust_main->ship_country,
+                                          'ship_',
+                                          'changed(this)', );
+
+  print "$ship_county_html $ship_state_html";
+
+  print qq!</TD><TH>${r}Zip</TH><TD><INPUT TYPE="text" NAME="ship_zip" VALUE="$ship_zip" SIZE=10 onChange="changed(this)"></TD></TR>!;
+
+  my($ship_daytime,$ship_night,$ship_fax)=(
+    $cust_main->ship_daytime,
+    $cust_main->ship_night,
+    $cust_main->ship_fax,
+  );
+
+  print <<END;
+  <TR><TH ALIGN="right">${r}Country</TH><TD>$ship_country_html</TD></TR>
+  <TR><TD ALIGN="right">$daytime_label</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_daytime" VALUE="$ship_daytime" SIZE=18 onChange="changed(this)"></TD></TR>
+  <TR><TD ALIGN="right">$night_label</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_night" VALUE="$ship_night" SIZE=18 onChange="changed(this)"></TD></TR>
+  <TR><TD ALIGN="right">Fax</TD><TD COLSPAN=5><INPUT TYPE="text" NAME="ship_fax" VALUE="$ship_fax" SIZE=12 onChange="changed(this)"></TD></TR>
+END
+
+  print "</TABLE>${r}required fields<BR>";
+
+}
+
+# billing info
+
+sub expselect {
+  my $prefix = shift;
+  my( $m, $y ) = (0, 0);
+  if ( scalar(@_) ) {
+    my $date = shift || '01-2000';
+    if ( $date  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #PostgreSQL date format
+      ( $m, $y ) = ( $2, $1 );
+    } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+      ( $m, $y ) = ( $1, $3 );
+    } else {
+      die "unrecognized expiration date format: $date";
+    }
+  }
+
+  my $return = qq!<SELECT NAME="$prefix!. qq!_month" SIZE="1">!;
+  for ( 1 .. 12 ) {
+    $return .= "<OPTION";
+    $return .= " SELECTED" if $_ == $m;
+    $return .= ">$_";
+  }
+  $return .= qq!</SELECT>/<SELECT NAME="$prefix!. qq!_year" SIZE="1">!;
+  for ( 2001 .. 2037 ) {
+    $return .= "<OPTION";
+    $return .= " SELECTED" if $_ == $y;
+    $return .= ">$_";
+  }
+  $return .= "</SELECT>";
+
+  $return;
+}
+
+my $payby_default = $conf->config('payby-default');
+
+if ( $payby_default eq 'HIDE' ) {
+
+  $cust_main->payby('BILL') unless $cust_main->payby;
+
+  foreach my $field (qw( tax payby )) {
+    print qq!<INPUT TYPE="hidden" NAME="$field" VALUE="!.
+          $cust_main->getfield($field). '">';
+  }
+
+  print qq!<INPUT TYPE="hidden" NAME="invoicing_list" VALUE="!.
+        join(', ', $cust_main->invoicing_list). '">';
+
+  foreach my $payby (qw( CARD DCRD CHEK DCHK LECB BILL COMP )) {
+    foreach my $field (qw( payinfo payname )) {
+      print qq!<INPUT TYPE="hidden" NAME="${payby}_$field" VALUE="!.
+            $cust_main->getfield($field). '">';
+    }
+
+    #false laziness w/expselect
+    my( $m, $y );
+    my $date = $cust_main->paydate || '12-2037';
+    if ( $date  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #PostgreSQL date format
+      ( $m, $y ) = ( $2, $1 );
+    } elsif ( $date =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
+      ( $m, $y ) = ( $1, $3 );
+    } else {
+      die "unrecognized expiration date format: $date";
+    }
+
+    print qq!<INPUT TYPE="hidden" NAME="${payby}_month" VALUE="$m">!.
+          qq!<INPUT TYPE="hidden" NAME="${payby}_year"  VALUE="$y">!;
+
+  }
+
+} else {
+
+  print "<BR>Billing information", &itable("#cccccc"),
+        qq!<TR><TD><INPUT TYPE="checkbox" NAME="tax" VALUE="Y"!;
+  print qq! CHECKED! if $cust_main->tax eq "Y";
+  print qq!>Tax Exempt</TD></TR><TR><TD>!.
+        qq!<INPUT TYPE="checkbox" NAME="invoicing_list_POST" VALUE="POST"!;
+
+  #my @invoicing_list = $cust_main->invoicing_list;
+  print qq! CHECKED!
+    if ( ! @invoicing_list && ! $conf->exists('disablepostalinvoicedefault') )
+       || grep { $_ eq 'POST' } @invoicing_list;
+  print qq!>Postal mail invoice</TD></TR>!;
+  my $invoicing_list = join(', ', grep { $_ ne 'POST' } @invoicing_list );
+  print qq!<TR><TD>Email invoice <INPUT TYPE="text" NAME="invoicing_list" VALUE="$invoicing_list"></TD></TR>!;
+
+  print "<TR><TD>Billing type</TD></TR>",
+        "</TABLE>",
+        &table("#cccccc"), "<TR>";
+
+  my($payinfo, $payname)=(
+    $cust_main->payinfo,
+    $cust_main->payname,
+  );
+
+  my %payby = (
+    'CARD' => qq!Credit card (automatic)<BR>${r}<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="" MAXLENGTH=19><BR>${r}Exp !. expselect("CARD"). qq!<BR>${r}Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="">!,
+    'DCRD' => qq!Credit card (on-demand)<BR>${r}<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="" MAXLENGTH=19><BR>${r}Exp !. expselect("DCRD"). qq!<BR>${r}Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="">!,
+    'CHEK' => qq!Electronic check (automatic)<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE=""><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="">!,
+    'DCHK' => qq!Electronic check (on-demand)<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE=""><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="">!,
+    'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
+    'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE=""><BR><INPUT TYPE="hidden" NAME="BILL_month" VALUE="12"><INPUT TYPE="hidden" NAME="BILL_year" VALUE="2037">Attention<BR><INPUT TYPE="text" NAME="BILL_payname" VALUE="">!,
+    'COMP' => qq!Complimentary<BR>${r}Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE=""><BR>${r}Exp !. expselect("COMP"),
+);
+
+  my( $account, $aba ) = split('@', $payinfo);
+
+  my %paybychecked = (
+    'CARD' => qq!Credit card (automatic)<BR>${r}<INPUT TYPE="text" NAME="CARD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR>${r}Exp !. expselect("CARD", $cust_main->paydate). qq!<BR>${r}Name on card<BR><INPUT TYPE="text" NAME="CARD_payname" VALUE="$payname">!,
+    'DCRD' => qq!Credit card (on-demand)<BR>${r}<INPUT TYPE="text" NAME="DCRD_payinfo" VALUE="$payinfo" MAXLENGTH=19><BR>${r}Exp !. expselect("DCRD", $cust_main->paydate). qq!<BR>${r}Name on card<BR><INPUT TYPE="text" NAME="DCRD_payname" VALUE="$payname">!,
+    'CHEK' => qq!Electronic check (automatic)<BR>${r}Account number <INPUT TYPE="text" NAME="CHEK_payinfo1" VALUE="$account"><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="CHEK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="CHEK_month" VALUE="12"><INPUT TYPE="hidden" NAME="CHEK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="CHEK_payname" VALUE="$payname">!,
+    'DCHK' => qq!Electronic check (on-demand)<BR>${r}Account number <INPUT TYPE="text" NAME="DCHK_payinfo1" VALUE="$account"><BR>${r}ABA/Routing code <INPUT TYPE="text" NAME="DCHK_payinfo2" VALUE="$aba" SIZE=10 MAXLENGTH=9><INPUT TYPE="hidden" NAME="DCHK_month" VALUE="12"><INPUT TYPE="hidden" NAME="DCHK_year" VALUE="2037"><BR>${r}Bank name <INPUT TYPE="text" NAME="DCHK_payname" VALUE="$payname">!,
+    'LECB' => qq!Phone bill billing<BR>${r}Phone number <INPUT TYPE="text" BANE="LECB_payinfo" VALUE="$payinfo" MAXLENGTH=15 SIZE=16><INPUT TYPE="hidden" NAME="LECB_month" VALUE="12"><INPUT TYPE="hidden" NAME="LECB_year" VALUE="2037"><INPUT TYPE="hidden" NAME="LECB_payname" VALUE="">!,
+    'BILL' => qq!Billing<BR>P.O. <INPUT TYPE="text" NAME="BILL_payinfo" VALUE="$payinfo"><BR><INPUT TYPE="hidden" NAME="BILL_month" VALUE="12"><INPUT TYPE="hidden" NAME="BILL_year" VALUE="2037">Attention<BR><INPUT TYPE="text" NAME="BILL_payname" VALUE="$payname">!,
+    'COMP' => qq!Complimentary<BR>${r}Approved by<INPUT TYPE="text" NAME="COMP_payinfo" VALUE="$payinfo"><BR>${r}Exp !. expselect("COMP", $cust_main->paydate),
+);
+
+  $cust_main->payby($payby_default) unless $cust_main->payby;
+  for (qw(CARD DCRD CHEK DCHK LECB BILL COMP)) {
+    print qq!<TD VALIGN=TOP><INPUT TYPE="radio" NAME="payby" VALUE="$_"!;
+    if ($cust_main->payby eq "$_") {
+      print qq! CHECKED> $paybychecked{$_}</TD>!;
+    } else {
+      print qq!> $payby{$_}</TD>!;
+    }
+  }
+
+  print "</TR></TABLE>$r required fields for each billing type";
+
+}
+
+if ( defined $cust_main->dbdef_table->column('comments') ) {
+    print "<BR><BR>Comments", &itable("#cccccc"),
+          qq!<TR><TD><TEXTAREA COLS=80 ROWS=5 WRAP="HARD" NAME="comments">!,
+          $cust_main->comments, "</TEXTAREA>",
+          "</TD></TR></TABLE>";
+}
+
+unless ( $custnum ) {
+  # pry the wrong place for this logic.  also pretty expensive
+  #use FS::part_pkg;
+
+  #false laziness, copied from FS::cust_pkg::order
+  my $pkgpart;
+  if ( scalar(@agents) == 1 ) {
+    # $pkgpart->{PKGPART} is true iff $custnum may purchase PKGPART
+    my($agent)=qsearchs('agent',{'agentnum'=> $agentnum });
+    $pkgpart = $agent->pkgpart_hashref;
+  } else {
+    #can't know (agent not chosen), so, allow all
+    my %typenum;
+    foreach my $agent ( @agents ) {
+      next if $typenum{$agent->typenum}++;
+      #fixed in 5.004_05 #$pkgpart->{$_}++ foreach keys %{ $agent->pkgpart_hashref }
+      foreach ( keys %{ $agent->pkgpart_hashref } ) { $pkgpart->{$_}++; } #5.004_04 workaround
+    }
+  }
+  #eslaf
+
+  my @part_pkg = grep { $_->svcpart('svc_acct') && $pkgpart->{ $_->pkgpart } }
+    qsearch( 'part_pkg', { 'disabled' => '' } );
+
+  if ( @part_pkg ) {
+
+#    print "<BR><BR>First package", &itable("#cccccc", "0 ALIGN=LEFT"),
+#apiabuse & undesirable wrapping
+    print "<BR><BR>First package", &itable("#cccccc"),
+          qq!<TR><TD COLSPAN=2><SELECT NAME="pkgpart_svcpart">!;
+
+    print qq!<OPTION VALUE="">(none)!;
+
+    foreach my $part_pkg ( @part_pkg ) {
+      print qq!<OPTION VALUE="!,
+#              $part_pkg->pkgpart. "_". $pkgpart{ $part_pkg->pkgpart }, '"';
+              $part_pkg->pkgpart. "_". $part_pkg->svcpart, '"';
+      print " SELECTED" if $saved_pkgpart && ( $part_pkg->pkgpart == $saved_pkgpart );
+      print ">", $part_pkg->pkg, " - ", $part_pkg->comment;
+    }
+    print "</SELECT></TD></TR>";
+
+    #false laziness: (mostly) copied from edit/svc_acct.cgi
+    #$ulen = $svc_acct->dbdef_table->column('username')->length;
+    my $ulen = dbdef->table('svc_acct')->column('username')->length;
+    my $ulen2 = $ulen+2;
+    my $passwordmax = $conf->config('passwordmax') || 8;
+    my $pmax2 = $passwordmax + 2;
+    print <<END;
+<TR><TD ALIGN="right">Username</TD>
+<TD><INPUT TYPE="text" NAME="username" VALUE="$username" SIZE=$ulen2 MAXLENGTH=$ulen></TD></TR>
+<TR><TD ALIGN="right">Password</TD>
+<TD><INPUT TYPE="text" NAME="_password" VALUE="$password" SIZE=$pmax2 MAXLENGTH=$passwordmax>
+(blank to generate)</TD></TR>
+END
+
+    print '<TR><TD ALIGN="right">Access number</TD><TD WIDTH="100%">'
+          .
+          &FS::svc_acct_pop::popselector($popnum).
+          '</TD></TR></TABLE>'
+          ;
+  }
+}
+
+my $otaker = $cust_main->otaker;
+print qq!<INPUT TYPE="hidden" NAME="otaker" VALUE="$otaker">!,
+      qq!<BR><INPUT TYPE="submit" VALUE="!,
+      $custnum ?  "Apply Changes" : "Add Customer", qq!">!,
+      "</FORM></BODY></HTML>",
+;
+
+%>
diff --git a/httemplate/edit/cust_main_county-expand.cgi b/httemplate/edit/cust_main_county-expand.cgi
new file mode 100755 (executable)
index 0000000..9f314a4
--- /dev/null
@@ -0,0 +1,54 @@
+<!-- mason kludge -->
+<%
+
+my($taxnum, $delim, $expansion, $taxclass );
+my($query) = $cgi->keywords;
+if ( $cgi->param('error') ) {
+  $taxnum = $cgi->param('taxnum');
+  $delim = $cgi->param('delim');
+  $expansion = $cgi->param('expansion');
+  $taxclass = $cgi->param('taxclass');
+} else {
+  $query =~ /^(taxclass)?(\d+)$/
+    or die "Illegal taxnum (query $query)";
+  $taxclass = $1 ? 'taxclass' : '';
+  $taxnum = $2;
+  $delim = 'n';
+  $expansion = '';
+}
+
+my $cust_main_county = qsearchs('cust_main_county',{'taxnum'=>$taxnum})
+  or die "cust_main_county.taxnum $taxnum not found";
+die "Can't expand entry!" if $cust_main_county->getfield('county');
+
+my $p1 = popurl(1);
+print header("Tax Rate (expand)", menubar(
+  'Main Menu' => popurl(2),
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print <<END;
+    <FORM ACTION="${p1}process/cust_main_county-expand.cgi" METHOD=POST>
+      <INPUT TYPE="hidden" NAME="taxnum" VALUE="$taxnum">
+      <INPUT TYPE="hidden" NAME="taxclass" VALUE="$taxclass">
+      Separate by
+END
+print '<INPUT TYPE="radio" NAME="delim" VALUE="n"';
+print ' CHECKED' if $delim eq 'n';
+print '>line (broken on some browsers) or',
+      '<INPUT TYPE="radio" NAME="delim" VALUE="s"';
+print ' CHECKED' if $delim eq 's';
+print '>whitespace.';
+print <<END;
+      <BR><INPUT TYPE="submit" VALUE="Submit">
+      <BR><TEXTAREA NAME="expansion" ROWS=100>$expansion</TEXTAREA>
+    </FORM>
+    </CENTER>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_main_county.cgi b/httemplate/edit/cust_main_county.cgi
new file mode 100755 (executable)
index 0000000..f3d2882
--- /dev/null
@@ -0,0 +1,69 @@
+<!-- mason kludge -->
+<%
+
+print header("Edit tax rates", menubar(
+  'Main Menu' => popurl(2),
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print qq!<FORM ACTION="!, popurl(1),
+    qq!process/cust_main_county.cgi" METHOD=POST>!, &table(), <<END;
+      <TR>
+        <TH><FONT SIZE=-1>Country</FONT></TH>
+        <TH><FONT SIZE=-1>State</FONT></TH>
+        <TH><FONT SIZE=-1>County</FONT></TH>
+        <TH><FONT SIZE=-1>Taxclass</FONT><BR><FONT SIZE=-2>(per-package classification)</FONT></TH>
+        <TH><FONT SIZE=-1>Tax name</FONT><BR><FONT SIZE=-2>(printed on invoices)</FONT></TH>
+        <TH><FONT SIZE=-1>Tax</FONT></TH>
+        <TH><FONT SIZE=-1>Exempt<BR>per<BR>month</TH>
+      </TR>
+END
+
+foreach my $cust_main_county ( sort {    $a->country cmp $b->country
+                                      or $a->state   cmp $b->state
+                                      or $a->county  cmp $b->county
+                                    } qsearch('cust_main_county',{}) ) {
+  my($hashref)=$cust_main_county->hashref;
+  print <<END;
+      <TR>
+        <TD BGCOLOR="#ffffff">$hashref->{country}</TD>
+END
+
+  print "<TD", $hashref->{state}
+      ? ' BGCOLOR="#ffffff">'.$hashref->{state}
+      : ' BGCOLOR="#cccccc">(ALL)'
+    , "</TD>";
+
+  print "<TD", $hashref->{county}
+      ? ' BGCOLOR="#ffffff">'. $hashref->{county}
+      : ' BGCOLOR="#cccccc">(ALL)'
+    , "</TD>";
+
+  print "<TD", $hashref->{taxclass}
+      ? ' BGCOLOR="#ffffff">'. $hashref->{taxclass}
+      : ' BGCOLOR="#cccccc">(ALL)'
+    , "</TD>";
+
+  print qq!<TD><INPUT TYPE="text" NAME="taxname!, $hashref->{taxnum},
+        qq!" VALUE="!, $hashref->{taxname}, qq!"></TD>!;
+  print qq!<TD><INPUT TYPE="text" NAME="tax!, $hashref->{taxnum},
+        qq!" VALUE="!, $hashref->{tax}, qq!" SIZE=6 MAXLENGTH=6>%</TD>!;
+  print qq!<TD>\$<INPUT TYPE="text" NAME="exempt_amount!, $hashref->{taxnum},
+        qq!" VALUE="!, $hashref->{exempt_amount}||0, qq!" SIZE=6></TD>!;
+  print '</TR>';
+
+}
+
+print <<END;
+    </TABLE>
+    <INPUT TYPE="submit" VALUE="Apply changes">
+    </FORM>
+    </CENTER>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_pay.cgi b/httemplate/edit/cust_pay.cgi
new file mode 100755 (executable)
index 0000000..f6ae7b2
--- /dev/null
@@ -0,0 +1,129 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my($link, $linknum, $paid, $payby, $payinfo, $quickpay); 
+if ( $cgi->param('error') ) {
+  $link = $cgi->param('link');
+  $linknum = $cgi->param('linknum');
+  $paid = $cgi->param('paid');
+  $payby = $cgi->param('payby');
+  $payinfo = $cgi->param('payinfo');
+  $quickpay = $cgi->param('quickpay');
+} elsif ($cgi->keywords) {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $link = 'invnum';
+  $linknum = $1;
+  $paid = '';
+  $payby = 'BILL';
+  $payinfo = "";
+  $quickpay = '';
+} elsif ( $cgi->param('custnum')  =~ /^(\d+)$/ ) {
+  $link = 'custnum';
+  $linknum = $1;
+  $paid = '';
+  $payby = 'BILL';
+  $payinfo = '';
+  $quickpay = $cgi->param('quickpay');
+} else {
+  die "illegal query ". $cgi->keywords;
+}
+my $_date = time;
+
+my $paybatch = "webui-$_date-$$-". rand() * 2**32;
+
+my $p1 = popurl(1);
+print header("Post payment", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT><BR><BR>"
+  if $cgi->param('error');
+
+print <<END, ntable("#cccccc",2);
+    <FORM ACTION="${p1}process/cust_pay.cgi" METHOD=POST>
+    <INPUT TYPE="hidden" NAME="link" VALUE="$link">
+    <INPUT TYPE="hidden" NAME="linknum" VALUE="$linknum">
+    <INPUT TYPE="hidden" NAME="quickpay" VALUE="$quickpay">
+END
+
+my $custnum;
+if ( $link eq 'invnum' ) {
+
+  my $cust_bill = qsearchs('cust_bill', { 'invnum' => $linknum } )
+    or die "unknown invnum $linknum";
+  print "Invoice #<B>$linknum</B>". ntable("#cccccc",2).
+        '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'.
+        time2str("%D", $cust_bill->_date). '</TD></TR>'.
+        '<TR><TD ALIGN="right" VALIGN="top">Items</TD><TD BGCOLOR="#ffffff">';
+  foreach ( $cust_bill->cust_bill_pkg ) { #false laziness with FS::cust_bill
+    if ( $_->pkgnum ) {
+
+      my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
+      my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
+      my($pkg)=$part_pkg->pkg;
+
+      if ( $_->setup != 0 ) {
+        print "$pkg Setup<BR>"; # $money_char. sprintf("%10.2f",$_->setup);
+        print join('<BR>',
+          map { "  ". $_->[0]. ": ". $_->[1] } $cust_pkg->labels
+        ). '<BR>';
+      }
+
+      if ( $_->recur != 0 ) {
+        print
+          "$pkg (" . time2str("%x",$_->sdate) . " - " .
+                                time2str("%x",$_->edate) . ")<BR>";
+          #$money_char. sprintf("%10.2f",$_->recur)
+        print join('<BR>',
+          map { '--->'. $_->[0]. ": ". $_->[1] } $cust_pkg->labels
+        ). '<BR>';
+      }
+
+    } else { #pkgnum Tax
+      print "Tax<BR>" # $money_char. sprintf("%10.2f",$_->setup)
+        if $_->setup != 0;
+    }
+
+  }
+  print '</TD></TR></TABLE><BR><BR>';
+
+  $custnum = $cust_bill->custnum;
+
+} elsif ( $link eq 'custnum' ) {
+  $custnum = $linknum;
+}
+
+print small_custview($custnum, $conf->config('countrydefault'));
+
+print qq!<INPUT TYPE="hidden" NAME="_date" VALUE="$_date">!;
+print qq!<INPUT TYPE="hidden" NAME="payby" VALUE="$payby">!;
+
+print '<BR><BR>Payment'. ntable("#cccccc", 2).
+      '<TR><TD ALIGN="right">Date</TD><TD BGCOLOR="#ffffff">'.
+      time2str("%D",$_date).  '</TD></TR>';
+
+print qq!<TR><TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">\$<INPUT TYPE="text" NAME="paid" VALUE="$paid" SIZE=8 MAXLENGTH=8></TD></TR>!;
+
+print qq!<TR><TD ALIGN="right">Payby</TD><TD BGCOLOR="#ffffff">$payby</TD></TR>!;
+
+#payinfo (check # now as payby="BILL" hardcoded.. what to do later?)
+print qq!<TR><TD ALIGN="right">Check #</TD><TD BGCOLOR="#ffffff"><INPUT TYPE="text" NAME="payinfo" VALUE="$payinfo"></TD></TR>!;
+
+print qq!<TR><TD ALIGN="right">Auto-apply<BR>to invoices</TD><TD><SELECT NAME="apply"><OPTION VALUE="yes" SELECTED>yes<OPTION>no</SELECT></TD>!;
+
+print "</TABLE>";
+
+#paybatch
+print qq!<INPUT TYPE="hidden" NAME="paybatch" VALUE="$paybatch">!;
+
+print <<END;
+<BR>
+<INPUT TYPE="submit" VALUE="Post payment">
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/cust_pkg.cgi b/httemplate/edit/cust_pkg.cgi
new file mode 100755 (executable)
index 0000000..485d601
--- /dev/null
@@ -0,0 +1,117 @@
+<!-- mason kludge -->
+<%
+
+my %pkg = ();
+my %comment = ();
+my %all_pkg = ();
+my %all_comment = ();
+#foreach (qsearch('part_pkg', { 'disabled' => '' })) {
+#  $pkg{ $_ -> getfield('pkgpart') } = $_->getfield('pkg');
+#  $comment{ $_ -> getfield('pkgpart') } = $_->getfield('comment');
+#}
+foreach (qsearch('part_pkg', {} )) {
+  $all_pkg{ $_ -> getfield('pkgpart') } = $_->getfield('pkg');
+  $all_comment{ $_ -> getfield('pkgpart') } = $_->getfield('comment');
+  next if $_->disabled;
+  $pkg{ $_ -> getfield('pkgpart') } = $_->getfield('pkg');
+  $comment{ $_ -> getfield('pkgpart') } = $_->getfield('comment');
+}
+
+my($custnum, %remove_pkg);
+if ( $cgi->param('error') ) {
+  $custnum = $cgi->param('custnum');
+  %remove_pkg = map { $_ => 1 } $cgi->param('remove_pkg');
+} else {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $custnum = $1;
+  %remove_pkg = ();
+}
+
+my $p1 = popurl(1);
+print header("Add/Edit Packages", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/cust_pkg.cgi" METHOD=POST>!;
+
+print qq!<INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!;
+
+#current packages
+my @cust_pkg = qsearch('cust_pkg',{ 'custnum' => $custnum, 'cancel' => '' } );
+
+if (@cust_pkg) {
+  print <<END;
+Current packages - select to remove (services are moved to a new package below)
+<BR><BR>
+END
+
+  my $count = 0 ;
+  print qq!<TABLE>! ;
+  foreach (@cust_pkg) {
+    print '<TR>' if $count == 0;
+    my($pkgnum,$pkgpart)=( $_->getfield('pkgnum'), $_->getfield('pkgpart') );
+    print qq!<TD><INPUT TYPE="checkbox" NAME="remove_pkg" VALUE="$pkgnum"!;
+    print " CHECKED" if $remove_pkg{$pkgnum};
+    print qq!>$pkgnum: $all_pkg{$pkgpart} - $all_comment{$pkgpart}</TD>\n!;
+    $count ++ ;
+    if ($count == 2)
+    {
+      $count = 0 ;
+      print qq!</TR>\n! ;
+    }
+  }
+  print qq!</TABLE><BR><BR>!;
+}
+
+print <<END;
+Order new packages<BR><BR>
+END
+
+my $cust_main = qsearchs('cust_main',{'custnum'=>$custnum});
+my $agent = qsearchs('agent',{'agentnum'=> $cust_main->agentnum });
+
+my $count = 0;
+my $pkgparts = 0;
+print qq!<TABLE>!;
+foreach my $type_pkgs ( qsearch('type_pkgs',{'typenum'=> $agent->typenum }) ) {
+  $pkgparts++;
+  my($pkgpart)=$type_pkgs->pkgpart;
+  next unless exists $pkg{$pkgpart}; #skip disabled ones
+  print qq!<TR>! if ( $count == 0 );
+  my $value = $cgi->param("pkg$pkgpart") || 0;
+  print <<END;
+  <TD>
+  <INPUT TYPE="text" NAME="pkg$pkgpart" VALUE="$value" SIZE="2" MAXLENGTH="2">
+  $pkgpart: $pkg{$pkgpart} - $comment{$pkgpart}</TD>\n
+END
+  $count ++ ;
+  if ( $count == 2 ) {
+    print qq!</TR>\n! ;
+    $count = 0;
+  }
+}
+print qq!</TABLE>!;
+
+unless ( $pkgparts ) {
+  my $p2 = popurl(2);
+  my $typenum = $agent->typenum;
+  my $agent_type = qsearchs( 'agent_type', { 'typenum' => $typenum } );
+  my $atype = $agent_type->atype;
+  print <<END;
+(No <a href="${p2}browse/part_pkg.cgi">package definitions</a>, or agent type
+<a href="${p2}edit/agent_type.cgi?$typenum">$atype</a> not allowed to purchase
+any packages.)
+END
+}
+
+#submit
+print <<END;
+<P><INPUT TYPE="submit" VALUE="Order">
+    </FORM>
+  </BODY>
+</HTML>
+END
+%>
diff --git a/httemplate/edit/msgcat.cgi b/httemplate/edit/msgcat.cgi
new file mode 100755 (executable)
index 0000000..ee9b1c6
--- /dev/null
@@ -0,0 +1,58 @@
+<!-- mason kludge -->
+<%
+
+print header("Edit Message catalog", menubar(
+#  'Main Menu' => $p,
+)), '<BR>';
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !. $cgi->param('error').
+      '</FONT><BR><BR>'
+  if $cgi->param('error');
+
+my $widget = new HTML::Widgets::SelectLayers(
+  'selected_layer' => 'en_US',
+  'options'        => { 'en_US'=>'en_US' },
+  'form_action'    => 'process/msgcat.cgi',
+  'layer_callback' => sub {
+    my $layer = shift;
+    my $html = qq!<INPUT TYPE="hidden" NAME="locale" VALUE="$layer">!.
+               "<BR>Messages for locale $layer<BR>". table().
+               "<TR><TH COLSPAN=2>Code</TH>".
+               "<TH>Message</TH>";
+    $html .= "<TH>en_US Message</TH>" unless $layer eq 'en_US';
+    $html .= '</TR>';
+
+    #foreach my $msgcat ( sort { $a->msgcode cmp $b->msgcode }
+    #                       qsearch('msgcat', { 'locale' => $layer } ) ) {
+    foreach my $msgcat ( qsearch('msgcat', { 'locale' => $layer } ) ) {
+      $html .=
+        '<TR><TD>'. $msgcat->msgnum. '</TD><TD>'. $msgcat->msgcode. '</TD>'.
+        '<TD><INPUT TYPE="text" SIZE=32 '.
+        qq! NAME="!. $msgcat->msgnum. '" '.
+        qq!VALUE="!. ($cgi->param($msgcat->msgnum)||$msgcat->msg). qq!"></TD>!;
+      unless ( $layer eq 'en_US' ) {
+        my $en_msgcat = qsearchs('msgcat', {
+          'locale'  => 'en_US',
+          'msgcode' => $msgcat->msgcode,
+        } );
+        $html .= '<TD>'. $en_msgcat->msg. '</TD>';
+      }
+      $html .= '</TR>';
+    }
+
+    $html .= '</TABLE><BR><INPUT TYPE="submit" VALUE="Apply changes">';
+
+    $html;
+  },
+
+);
+
+print $widget->html;
+
+print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/part_bill_event.cgi b/httemplate/edit/part_bill_event.cgi
new file mode 100755 (executable)
index 0000000..6426eed
--- /dev/null
@@ -0,0 +1,222 @@
+<!-- mason kludge -->
+<%
+
+if ( $cgi->param('eventpart') && $cgi->param('eventpart') =~ /^(\d+)$/ ) {
+  $cgi->param('eventpart', $1);
+} else {
+  $cgi->param('eventpart', '');
+}
+
+my ($query) = $cgi->keywords;
+my $action = '';
+my $part_bill_event = '';
+if ( $cgi->param('error') ) {
+  $part_bill_event = new FS::part_bill_event ( {
+    map { $_, scalar($cgi->param($_)) } fields('part_bill_event')
+  } );
+}
+if ( $query && $query =~ /^(\d+)$/ ) {
+  $part_bill_event ||= qsearchs('part_bill_event',{'eventpart'=>$1});
+} else {
+  $part_bill_event ||= new FS::part_bill_event {};
+}
+$action ||= $part_bill_event->pkgpart ? 'Edit' : 'Add';
+my $hashref = $part_bill_event->hashref;
+
+print header("$action Invoice Event Definition", menubar(
+  'Main Menu' => popurl(2),
+  'View all invoice events' => popurl(2). 'browse/part_bill_event.cgi',
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print '<FORM ACTION="', popurl(1), 'process/part_bill_event.cgi" METHOD=POST>'.
+      '<INPUT TYPE="hidden" NAME="eventpart" VALUE="'.
+      $part_bill_event->eventpart  .'">';
+print "Invoice Event #", $hashref->{eventpart} ? $hashref->{eventpart} : "(NEW)";
+
+print ntable("#cccccc",2), <<END;
+<TR><TD ALIGN="right">Payby</TD><TD><SELECT NAME="payby">
+END
+
+for (qw(CARD DCRD CHEK DCHK LECB BILL COMP)) {
+  print qq!<OPTION VALUE="$_"!;
+  if ($part_bill_event->payby eq $_) {
+    print " SELECTED>$_</OPTION>";
+  } else {
+    print ">$_</OPTION>";
+  }
+}
+
+my $days = $hashref->{seconds}/86400;
+
+print <<END;
+</SELECT></TD></TR>
+<TR><TD ALIGN="right">Event</TD><TD><INPUT TYPE="text" NAME="event" VALUE="$hashref->{event}"></TD></TR>
+<TR><TD ALIGN="right">After</TD><TD><INPUT TYPE="text" NAME="days" VALUE="$days"> days</TD></TR>
+END
+
+print '<TR><TD ALIGN="right">Disabled</TD><TD>';
+print '<INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"';
+print ' CHECKED' if $hashref->{disabled} eq "Y";
+print '>';
+print '</TD></TR>';
+
+print '<TR><TD ALIGN="right">Action</TD><TD>';
+
+#print ntable();
+
+#this is pretty kludgy right here.
+tie my %events, 'Tie::IxHash',
+
+  'fee' => {
+    'name'   => 'Late fee',
+    'code'   => '$cust_main->charge( %%%charge%%%, \'%%%reason%%%\' );',
+    'html'   => 
+      'Amount <INPUT TYPE="text" SIZE="7" NAME="charge" VALUE="%%%charge%%%">'.
+      '<BR>Reason <INPUT TYPE="text" NAME="reason" VALUE="%%%reason%%%">',
+    'weight' => 10,
+  },
+  'suspend' => {
+    'name'   => 'Suspend',
+    'code'   => '$cust_main->suspend();',
+    'weight' => 10,
+  },
+  'cancel' => {
+    'name'   => 'Cancel',
+    'code'   => '$cust_main->cancel();',
+    'weight' => 10,
+  },
+
+  'addpost' => {
+    'name' => 'Add postal invoicing',
+    'code' => '$cust_main->invoicing_list_addpost(); "";',
+    'weight'  => 20,
+  },
+
+  'comp' => {
+    'name' => 'Pay invoice with a complimentary "payment"',
+    'code' => '$cust_bill->comp();',
+    'weight' => 30,
+  },
+
+  'realtime-card' => {
+    'name' => 'Run card with a <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> realtime gateway',
+    'code' => '$cust_bill->realtime_card();',
+    'weight' => 30,
+  },
+
+  'realtime-check' => {
+    'name' => 'Run check with a <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> realtime gateway',
+    'code' => '$cust_bill->realtime_ach();',
+    'weight' => 30,
+  },
+
+  'realtime-lec' => {
+    'name' => 'Run phone bill ("LEC") billing with a <a href="http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment">Business::OnlinePayment</a> realtime gateway',
+    'code' => '$cust_bill->realtime_lec();',
+    'weight' => 30,
+  },
+
+  'batch-card' => {
+    'name' => 'Add card to the pending credit card batch',
+    'code' => '$cust_bill->batch_card();',
+    'weight' => 40,
+  },
+
+  'send' => {
+    'name' => 'Send invoice (email/print)',
+    'code' => '$cust_bill->send();',
+    'weight' => 50,
+  },
+
+  'send_alternate' => {
+    'name' => 'Send invoice (email/print) with alternate template',
+    'code' => '$cust_bill->send(\'%%%templatename%%%\');',
+    'html' =>
+        '<INPUT TYPE="text" NAME="templatename" VALUE="%%%templatename%%%">',
+    'weight' => 50,
+  },
+
+  'send_csv_ftp' => {
+    'name' => 'Upload CSV invoice data to an FTP server',
+    'code' => '$cust_bill->send_csv( protocol => \'ftp\',
+                                     server   => \'%%%ftpserver%%%\',
+                                     username => \'%%%ftpusername%%%\',
+                                     password => \'%%%ftppassword%%%\',
+                                     dir      => \'%%%ftpdir%%%\'       );',
+    'html' =>
+        '<TABLE BORDER=0><TR><TD ALIGN="right">FTP server: </TD>'.
+          '<TD><INPUT TYPE="text" NAME="ftpserver" VALUE="%%%ftpserver%%%">'.
+          '</TD></TR>'.
+        '<TR><TD ALIGN="right">FTP username: </TD><TD>'.
+          '<INPUT TYPE="text" NAME="ftpusername" VALUE="%%%ftpusername%%%">'.
+          '</TD></TR>'.
+        '<TR><TD ALIGN="right">FTP password: </TD><TD>'.
+          '<INPUT TYPE="text" NAME="ftppassword" VALUE="%%%ftppassword%%%">'.
+          '</TD></TR>'.
+        '<TR><TD ALIGN="right">FTP directory: </TD>'.
+          '<TD><INPUT TYPE="text" NAME="ftpdir" VALUE="%%%ftpdir%%%">'.
+          '</TD></TR>'.
+        '</TABLE>',
+    'weight' => 50,
+  },
+
+  'bill' => {
+    'name' => 'Generate invoices (normally only used with a <i>Late Fee</i> event)',
+    'code' => '$cust_main->bill();',
+    'weight'  => 60,
+  },
+
+  'apply' => {
+    'name' => 'Apply unapplied payments and credits',
+    'code' => '$cust_main->apply_payments; $cust_main->apply_credits; "";',
+    'weight'  => 70,
+  },
+
+  'collect' => {
+    'name' => 'Collect on invoices (normally only used with a <i>Late Fee</i> and <i>Generate Invoice</i> events)',
+    'code' => '$cust_main->collect();',
+    'weight'  => 80,
+  },
+
+;
+
+foreach my $event ( keys %events ) {
+  my %plandata = map { /^(\w+) (.*)$/; ($1, $2); }
+                   split(/\n/, $part_bill_event->plandata);
+  my $html = $events{$event}{html};
+  while ( $html =~ /%%%(\w+)%%%/ ) {
+    my $field = $1;
+    $html =~ s/%%%$field%%%/$plandata{$field}/;
+  }
+
+  print ntable( "#cccccc", 2).
+        qq!<TR><TD><INPUT TYPE="radio" NAME="plan_weight_eventcode" !;
+  print "CHECKED " if $event eq $part_bill_event->plan;
+  print qq!VALUE="!.  $event. ":". $events{$event}{weight}. ":".
+        encode_entities($events{$event}{code}).
+        qq!">$events{$event}{name}</TD>!;
+  print '<TD>'. $html. '</TD>' if $html;
+  print qq!</TR>!;
+  print '</TABLE>';
+}
+
+#print '</TABLE>';
+
+print <<END;
+</TD></TR>
+</TABLE>
+END
+
+print qq!<INPUT TYPE="submit" VALUE="!,
+      $hashref->{eventpart} ? "Apply changes" : "Add invoice event",
+      qq!">!;
+%>
+
+    </FORM>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi
new file mode 100644 (file)
index 0000000..cc60f1a
--- /dev/null
@@ -0,0 +1,125 @@
+<!-- mason kludge -->
+<%
+
+#if ( $cgi->param('clone') && $cgi->param('clone') =~ /^(\d+)$/ ) {
+#  $cgi->param('clone', $1);
+#} else {
+#  $cgi->param('clone', '');
+#}
+
+my($query) = $cgi->keywords;
+my $action = '';
+my $part_export = '';
+if ( $cgi->param('error') ) {
+  $part_export = new FS::part_export ( {
+    map { $_, scalar($cgi->param($_)) } fields('part_export')
+  } );
+} elsif ( $query =~ /^(\d+)$/ ) {
+  $part_export = qsearchs('part_export', { 'exportnum' => $1 } );
+} else {
+  $part_export = new FS::part_export;
+}
+$action ||= $part_export->exportnum ? 'Edit' : 'Add';
+
+#my $exports = FS::part_export::export_info($svcdb);
+my $exports = FS::part_export::export_info();
+
+my %layers = map { $_ => "$_ - ". $exports->{$_}{desc} } keys %$exports;
+$layers{''}='';
+
+my $widget = new HTML::Widgets::SelectLayers(
+  'selected_layer' => $part_export->exporttype,
+  'options'        => \%layers,
+  'form_name'      => 'dummy',
+  'form_action'    => 'process/part_export.cgi',
+  'form_text'      => [qw( exportnum machine )],
+#  'form_checkbox'  => [qw()],
+  'html_between'    => "</TD></TR></TABLE>\n",
+  'layer_callback'  => sub {
+    my $layer = shift;
+    my $html = qq!<INPUT TYPE="hidden" NAME="exporttype" VALUE="$layer">!.
+               ntable("#cccccc",2);
+
+    $html .= '<TR><TD ALIGN="right">Description</TD><TD BGCOLOR=#ffffff>'.
+             $exports->{$layer}{notes}. '</TD></TR>'
+      if $layer;
+
+    foreach my $option ( keys %{$exports->{$layer}{options}} ) {
+      my $optinfo = $exports->{$layer}{options}{$option};
+      my $label = $optinfo->{label};
+      my $type = defined($optinfo->{type}) ? $optinfo->{type} : 'text';
+      my $value = $cgi->param($option)
+                 || ( $part_export->exportnum && $part_export->option($option) )
+                 || ( (exists $optinfo->{default} && !$part_export->exportnum)
+                      ? $optinfo->{default}
+                      : ''
+                    );
+      $html .= qq!<TR><TD ALIGN="right">$label</TD><TD>!;
+      if ( $type eq 'select' ) {
+        $html .= qq!<SELECT NAME="$option">!;
+        foreach my $select_option ( @{$optinfo->{options}} ) {
+          #if ( ref($select_option) ) {
+          #} else {
+            my $selected = $select_option eq $value ? ' SELECTED' : '';
+            $html .= qq!<OPTION VALUE="$select_option"$selected>!.
+                     qq!$select_option</OPTION>!;
+          #}
+        }
+        $html .= '</SELECT>';
+      } elsif ( $type eq 'textarea' ) {
+        $html .= qq!<TEXTAREA NAME="$option" COLS=80 ROWS=8 WRAP="virtual">!.
+                 qq!$value</TEXTAREA>!;
+      } elsif ( $type eq 'text' ) {
+        $html .= qq!<INPUT TYPE="text" NAME="$option" VALUE="$value" SIZE=64>!;
+      } elsif ( $type eq 'checkbox' ) {
+        $html .= qq!<INPUT TYPE="checkbox" NAME="$option" VALUE="1"!;
+        $html .= ' CHECKED' if $value;
+        $html .= '>';
+      } else {
+        $html .= "unknown type $type";
+      }
+      $html .= '</TD></TR>';
+    }
+    $html .= '</TABLE>';
+
+    $html .= '<INPUT TYPE="hidden" NAME="options" VALUE="'.
+             join(',', keys %{$exports->{$layer}{options}} ). '">';
+
+    $html .= '<INPUT TYPE="hidden" NAME="nodomain" VALUE="'.
+             $exports->{$layer}{nodomain}. '">';
+
+    $html .= '<INPUT TYPE="submit" VALUE="'.
+             ( $part_export->exportnum ? "Apply changes" : "Add export" ).
+             '">';
+
+    $html;
+  },
+);
+
+%>
+<%= header("$action Export", menubar(
+  'Main Menu' => popurl(2),
+), ' onLoad="visualize()"')
+%>
+
+<% if ( $cgi->param('error') ) { %>
+  <FONT SIZE="+1" COLOR="#ff0000">Error: <%= $cgi->param('error') %></FONT>
+  <BR><BR>
+<% } %>
+
+<FORM NAME="dummy">
+<INPUT TYPE="hidden" NAME="exportnum" VALUE="<%= $part_export->exportnum %>">
+
+<%= ntable("#cccccc",2) %>
+<TR>
+  <TD ALIGN="right">Export host</TD>
+  <TD>
+    <INPUT TYPE="text" NAME="machine" VALUE="<%= $part_export->machine %>">
+  </TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Export</TD>
+  <TD><%= $widget->html %>
+</BODY>
+</HTML>
+
diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi
new file mode 100755 (executable)
index 0000000..dee3562
--- /dev/null
@@ -0,0 +1,508 @@
+<!-- mason kludge -->
+<%
+
+if ( $cgi->param('clone') && $cgi->param('clone') =~ /^(\d+)$/ ) {
+  $cgi->param('clone', $1);
+} else {
+  $cgi->param('clone', '');
+}
+if ( $cgi->param('pkgnum') && $cgi->param('pkgnum') =~ /^(\d+)$/ ) {
+  $cgi->param('pkgnum', $1);
+} else {
+  $cgi->param('pkgnum', '');
+}
+
+my ($query) = $cgi->keywords;
+my $action = '';
+my $part_pkg = '';
+if ( $cgi->param('error') ) {
+  $part_pkg = new FS::part_pkg ( {
+    map { $_, scalar($cgi->param($_)) } fields('part_pkg')
+  } );
+}
+if ( $cgi->param('clone') ) {
+  $action='Custom Pricing';
+  my $old_part_pkg =
+    qsearchs('part_pkg', { 'pkgpart' => $cgi->param('clone') } );
+  $part_pkg ||= $old_part_pkg->clone;
+  $part_pkg->disabled('Y');
+} elsif ( $query && $query =~ /^(\d+)$/ ) {
+  $part_pkg ||= qsearchs('part_pkg',{'pkgpart'=>$1});
+} else {
+  unless ( $part_pkg ) {
+    $part_pkg = new FS::part_pkg {};
+    $part_pkg->plan('flat');
+  }
+}
+unless ( $part_pkg->plan ) { #backwards-compat
+  $part_pkg->plan('flat');
+  $part_pkg->plandata("setup_fee=". $part_pkg->setup. "\n".
+                      "recur_fee=". $part_pkg->recur. "\n");
+}
+$action ||= $part_pkg->pkgpart ? 'Edit' : 'Add';
+my $hashref = $part_pkg->hashref;
+
+
+print header("$action Package Definition", menubar(
+  'Main Menu' => popurl(2),
+  'View all packages' => popurl(2). 'browse/part_pkg.cgi',
+));
+#), ' onLoad="visualize()"');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+#print '<FORM ACTION="', popurl(1), 'process/part_pkg.cgi" METHOD=POST>';
+print '<FORM NAME="dummy">';
+
+#if ( $cgi->param('clone') ) {
+#  print qq!<INPUT TYPE="hidden" NAME="clone" VALUE="!, $cgi->param('clone'), qq!">!;
+#}
+#if ( $cgi->param('pkgnum') ) {
+#  print qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="!, $cgi->param('pkgnum'), qq!">!;
+#}
+#
+#print qq!<INPUT TYPE="hidden" NAME="pkgpart" VALUE="$hashref->{pkgpart}">!,
+print "Package Part #", $hashref->{pkgpart} ? $hashref->{pkgpart} : "(NEW)";
+
+print ntable("#cccccc",2), <<END;
+<TR><TD ALIGN="right">Package (customer-visible)</TD><TD><INPUT TYPE="text" NAME="pkg" SIZE=32 VALUE="$hashref->{pkg}"></TD></TR>
+<TR><TD ALIGN="right">Comment (customer-hidden)</TD><TD><INPUT TYPE="text" NAME="comment" SIZE=32 VALUE="$hashref->{comment}"></TD></TR>
+<TR><TD ALIGN="right">Frequency (months) of recurring fee</TD><TD><INPUT TYPE="text" NAME="freq" VALUE="$hashref->{freq}" SIZE=3>&nbsp;&nbsp;<I>0=no recurring fee, 1=monthly, 3=quarterly, 12=yearly</TD></TR>
+<TR><TD ALIGN="right">Setup fee tax exempt</TD><TD>
+END
+
+print '<INPUT TYPE="checkbox" NAME="setuptax" VALUE="Y"';
+print ' CHECKED' if $hashref->{setuptax} eq "Y";
+print '>';
+
+print <<END;
+</TD></TR>
+<TR><TD ALIGN="right">Recurring fee tax exempt</TD><TD>
+END
+
+print '<INPUT TYPE="checkbox" NAME="recurtax" VALUE="Y"';
+print ' CHECKED' if $hashref->{recurtax} eq "Y";
+print '>';
+
+print '</TD></TR>';
+
+my $conf = new FS::Conf;
+#false laziness w/ view/cust_main.cgi quick order
+if ( $conf->exists('enable_taxclasses') ) {
+  print '<TR><TD ALIGN="right">Tax class</TD><TD><SELECT NAME="taxclass">';
+  my $sth = dbh->prepare('SELECT DISTINCT taxclass FROM cust_main_county')
+    or die dbh->errstr;
+  $sth->execute or die $sth->errstr;
+  foreach my $taxclass ( map $_->[0], @{$sth->fetchall_arrayref} ) {
+    print qq!<OPTION VALUE="$taxclass"!;
+    print ' SELECTED' if $taxclass eq $hashref->{taxclass};
+    print qq!>$taxclass</OPTION>!;
+  }
+  print '</SELECT></TD></TR>';
+} else {
+  print
+    '<INPUT TYPE="hidden" NAME="taxclass" VALUE="'. $hashref->{taxclass}. '">';
+}
+
+print '<TR><TD ALIGN="right">Disable new orders</TD><TD>';
+print '<INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"';
+print ' CHECKED' if $hashref->{disabled} eq "Y";
+print '>';
+print '</TD></TR></TABLE>';
+
+my $thead =  "\n\n". ntable('#cccccc', 2). <<END;
+<TR><TH BGCOLOR="#dcdcdc"><FONT SIZE=-1>Quan.</FONT></TH><TH BGCOLOR="#dcdcdc">Service</TH></TR>
+END
+
+#unless ( $cgi->param('clone') ) {
+#dunno why...
+unless ( 0 ) {
+  #print <<END, $thead;
+  print <<END, itable(), '<TR><TD VALIGN="top">', $thead;
+<BR><BR>Enter the quantity of each service this package includes.<BR><BR>
+END
+}
+
+my @fixups = ();
+my $count = 0;
+my $columns = 3;
+my @part_svc = qsearch( 'part_svc', { 'disabled' => '' } );
+foreach my $part_svc ( @part_svc ) {
+  my $svcpart = $part_svc->svcpart;
+  my $pkgpart = $cgi->param('clone') || $part_pkg->pkgpart;
+  my $pkg_svc = $pkgpart && qsearchs( 'pkg_svc', {
+    'pkgpart'  => $pkgpart,
+    'svcpart'  => $svcpart,
+  } ) || new FS::pkg_svc ( {
+    'pkgpart'  => $pkgpart,
+    'svcpart'  => $svcpart,
+    'quantity' => 0,
+  });
+  #? #next unless $pkg_svc;
+
+  push @fixups, "pkg_svc$svcpart";
+
+  #unless ( defined ($cgi->param('clone')) && $cgi->param('clone') ) {
+  #dunno why...
+  unless ( 0 ) {
+    print '<TR>'; # if $count == 0 ;
+    print qq!<TD><INPUT TYPE="text" NAME="pkg_svc$svcpart" SIZE=4 MAXLENGTH=3 VALUE="!,
+          $cgi->param("pkg_svc$svcpart") || $pkg_svc->quantity || 0,
+          qq!"></TD><TD><A HREF="part_svc.cgi?!,$part_svc->svcpart,
+          qq!">!, $part_svc->getfield('svc'), "</A></TD></TR>";
+#    print "</TABLE></TD><TD>$thead" if ++$count == int(scalar(@part_svc) / 2);
+    $count+=1;
+    foreach ( 1 .. $columns-1 ) {
+      print "</TABLE></TD><TD VALIGN=\"top\">$thead"
+        if $count == int( $_ * scalar(@part_svc) / $columns );
+    }
+  } else {
+    print qq!<INPUT TYPE="hidden" NAME="pkg_svc$svcpart" VALUE="!,
+          $cgi->param("pkg_svc$svcpart") || $pkg_svc->quantity || 0, qq!">\n!;
+  }
+}
+
+#unless ( $cgi->param('clone') ) {
+#dunno why...
+unless ( 0 ) {
+  print "</TR></TABLE></TD></TR></TABLE>";
+  #print "</TR></TABLE>";
+}
+
+foreach my $f ( qw( clone pkgnum ) ) {
+  print qq!<INPUT TYPE="hidden" NAME="$f" VALUE="!. $cgi->param($f). '">';
+}
+print '<INPUT TYPE="hidden" NAME="pkgpart" VALUE="'. $part_pkg->pkgpart. '">';
+
+# prolly should be in database
+tie my %plans, 'Tie::IxHash',
+  'flat' => {
+    'name' => 'Flat rate (anniversary billing)',
+    'fields' => {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_fee' => { 'name' => 'Recurring fee for this package',
+                       'default' => 0,
+                      },
+    },
+    'fieldorder' => [ 'setup_fee', 'recur_fee' ],
+    'setup' => 'what.setup_fee.value',
+    'recur' => 'what.recur_fee.value',
+  },
+
+  'flat_delayed' => {
+    'name' => 'Free for X days, then flat rate (anniversary billing)',
+    'fields' =>  {
+      'free_days' => { 'name' => 'Initial free days',
+                       'default' => 0,
+                     },
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_fee' => { 'name' => 'Recurring fee for this package',
+                       'default' => 0,
+                      },
+    },
+    'fieldorder' => [ 'free_days', 'setup_fee', 'recur_fee' ],
+    'setup' => '\'my $d = $cust_pkg->bill || $time; $d += 86400 * \' + what.free_days.value + \'; $cust_pkg->bill($d); $cust_pkg_mod_flag=1; \' + what.setup_fee.value',
+    'recur' => 'what.recur_fee.value',
+  },
+
+  'prorate' => {
+    'name' => 'First partial month pro-rated, then flat-rate (1st of month billing)',
+    'fields' =>  {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_fee' => { 'name' => 'Recurring fee for this package',
+                       'default' => 0,
+                      },
+    },
+    'fieldorder' => [ 'setup_fee', 'recur_fee' ],
+    'setup' => 'what.setup_fee.value',
+    'recur' => '\'my $mnow = $sdate; my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($sdate) )[0,1,2,3,4,5]; my $mstart = timelocal(0,0,0,1,$mon,$year); my $mend = timelocal(0,0,0,1, $mon == 11 ? 0 : $mon+1, $year+($mon==11)); $sdate = $mstart; ( $part_pkg->freq - 1 ) * \' + what.recur_fee.value + \' / $part_pkg->freq + \' + what.recur_fee.value + \' / $part_pkg->freq * ($mend-$mnow) / ($mend-$mstart) ; \'',
+  },
+
+  'subscription' => {
+    'name' => 'First partial month full charge, then flat-rate (1st of month billing)',
+    'fields' => {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_fee' => { 'name' => 'Recurring fee for this package',
+                       'default' => 0,
+                      },
+    },
+    'fieldorder' => [ 'setup_fee', 'recur_fee' ],
+    'setup' => 'what.setup_fee.value',
+    'recur' => '\'my $mnow = $sdate; my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($sdate) )[0,1,2,3,4,5]; $sdate = timelocal(0,0,0,1,$mon,$year); \' + what.recur_fee.value',
+  },
+
+  'flat_comission_cust' => {
+    'name' => 'Flat rate with recurring commission per active customer',
+    'fields' => {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_fee' => { 'name' => 'Recurring fee for this package',
+                       'default' => 0,
+                     },
+      'comission_amount' => { 'name' => 'Commission amount per month (per active customer)',
+                              'default' => 0,
+                            },
+      'comission_depth'  => { 'name' => 'Number of layers',
+                              'default' => 1,
+                            },
+    },
+    'fieldorder' => [ 'setup_fee', 'recur_fee', 'comission_depth', 'comission_amount' ],
+    'setup' => 'what.setup_fee.value',
+    'recur' => '\'my $error = $cust_pkg->cust_main->credit( \' + what.comission_amount.value + \' * scalar($cust_pkg->cust_main->referral_cust_main_ncancelled(\' + what.comission_depth.value+ \')), "commission" ); die $error if $error; \' + what.recur_fee.value + \';\'',
+  },
+
+  'flat_comission' => {
+    'name' => 'Flat rate with recurring commission per (any) active package',
+    'fields' => {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_fee' => { 'name' => 'Recurring fee for this package',
+                       'default' => 0,
+                     },
+      'comission_amount' => { 'name' => 'Commission amount per month (per active package)',
+                              'default' => 0,
+                            },
+      'comission_depth'  => { 'name' => 'Number of layers',
+                              'default' => 1,
+                            },
+    },
+    'fieldorder' => [ 'setup_fee', 'recur_fee', 'comission_depth', 'comission_amount' ],
+    'setup' => 'what.setup_fee.value',
+    'recur' => '\'my $error = $cust_pkg->cust_main->credit( \' + what.comission_amount.value + \' * scalar($cust_pkg->cust_main->referral_cust_pkg(\' + what.comission_depth.value+ \')), "commission" ); die $error if $error; \' + what.recur_fee.value + \';\'',
+  },
+
+  'flat_comission_pkg' => {
+    'name' => 'Flat rate with recurring commission per (selected) active package',
+    'fields' => {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_fee' => { 'name' => 'Recurring fee for this package',
+                       'default' => 0,
+                     },
+      'comission_amount' => { 'name' => 'Commission amount per month (per uncancelled package)',
+                              'default' => 0,
+                            },
+      'comission_depth'  => { 'name' => 'Number of layers',
+                              'default' => 1,
+                            },
+      'comission_pkgpart' => { 'name' => 'Applicable packages<BR><FONT SIZE="-1">(hold <b>ctrl</b> to select multiple packages)</FONT>',
+                               'type' => 'select_multiple',
+                               'select_table' => 'part_pkg',
+                               'select_hash'  => { 'disabled' => '' } ,
+                               'select_key'   => 'pkgpart',
+                               'select_label' => 'pkg',
+                             },
+    },
+    'fieldorder' => [ 'setup_fee', 'recur_fee', 'comission_depth', 'comission_amount', 'comission_pkgpart' ],
+    'setup' => 'what.setup_fee.value',
+    'recur' => '""; var pkgparts = ""; for ( var c=0; c < document.flat_comission_pkg.comission_pkgpart.options.length; c++ ) { if (document.flat_comission_pkg.comission_pkgpart.options[c].selected) { pkgparts = pkgparts + document.flat_comission_pkg.comission_pkgpart.options[c].value + \', \'; } } what.recur.value = \'my $error = $cust_pkg->cust_main->credit( \' + what.comission_amount.value + \' * scalar( grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } ( \' + pkgparts + \'  ) } $cust_pkg->cust_main->referral_cust_pkg(\' + what.comission_depth.value+ \')), "commission" ); die $error if $error; \' + what.recur_fee.value + \';\'',
+  },
+
+
+
+  'sesmon_hour' => {
+    'name' => 'Base charge plus charge per-hour from the session monitor',
+    'fields' => {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_flat' => { 'name' => 'Base monthly charge for this package',
+                        'default' => 0,
+                      },
+      'recur_included_hours' => { 'name' => 'Hours included',
+                                  'default' => 0,
+                                },
+      'recur_hourly_charge' => { 'name' => 'Additional charge per hour',
+                                 'default' => 0,
+                               },
+    },
+    'fieldorder' => [ 'setup_fee', 'recur_flat', 'recur_included_hours', 'recur_hourly_charge' ],
+    'setup' => 'what.setup_fee.value',
+    'recur' => '\'my $hours = $cust_pkg->seconds_since($cust_pkg->bill || 0) / 3600 - \' + what.recur_included_hours.value + \'; $hours = 0 if $hours < 0; \' + what.recur_flat.value + \' + \' + what.recur_hourly_charge.value + \' * $hours;\'',
+  },
+
+  'sesmon_minute' => {
+    'name' => 'Base charge plus charge per-minute from the session monitor',
+    'fields' => {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_flat' => { 'name' => 'Base monthly charge for this package',
+                        'default' => 0,
+                      },
+      'recur_included_min' => { 'name' => 'Minutes included',
+                                'default' => 0,
+                                },
+      'recur_minly_charge' => { 'name' => 'Additional charge per minute',
+                                'default' => 0,
+                              },
+    },
+    'fieldorder' => [ 'setup_fee', 'recur_flat', 'recur_included_min', 'recur_minly_charge' ],
+    'setup' => 'what.setup_fee.value',
+    'recur' => '\'my $min = $cust_pkg->seconds_since($cust_pkg->bill || 0) / 60 - \' + what.recur_included_min.value + \'; $min = 0 if $min < 0; \' + what.recur_flat.value + \' + \' + what.recur_minly_charge.value + \' * $min;\'',
+
+  },
+
+  'sqlradacct_hour' => {
+    'name' => 'Base charge plus charge per-hour (and for data) from an external sqlradius radacct table',
+    'fields' => {
+      'setup_fee' => { 'name' => 'Setup fee for this package',
+                       'default' => 0,
+                     },
+      'recur_flat' => { 'name' => 'Base monthly charge for this package',
+                        'default' => 0,
+                      },
+      'recur_included_hours' => { 'name' => 'Hours included',
+                                  'default' => 0,
+                                },
+      'recur_hourly_charge' => { 'name' => 'Additional charge per hour',
+                                 'default' => 0,
+                               },
+      'recur_included_input' => { 'name' => 'Input megabytes included',
+                                  'default' => 0,
+                                },
+      'recur_input_charge' => { 'name' =>
+                                        'Additional charge per input megabyte',
+                                'default' => 0,
+                              },
+      'recur_included_output' => { 'name' => 'Output megabytes included',
+                                   'default' => 0,
+                                },
+      'recur_output_charge' => { 'name' =>
+                                       'Additional charge per output megabyte',
+                                'default' => 0,
+                              },
+      'recur_included_total' => { 'name' =>
+                                       'Total input+output megabytes included',
+                                  'default' => 0,
+                                },
+      'recur_total_charge' => { 'name' =>
+                                 'Additional charge per input+output megabyte',
+                                'default' => 0,
+                              },
+    },
+    'fieldorder' => [qw( setup_fee recur_flat recur_included_hours recur_hourly_charge recur_included_input recur_input_charge recur_included_output recur_output_charge recur_included_total recur_total_charge )],
+    'setup' => 'what.setup_fee.value',
+    'recur' => '\'my $last_bill = $cust_pkg->last_bill; my $hours = $cust_pkg->seconds_since_sqlradacct($last_bill, $sdate ) / 3600 - \' + what.recur_included_hours.value + \'; $hours = 0 if $hours < 0; my $input = $cust_pkg->attribute_since_sqlradacct($last_bill, $sdate, \"AcctInputOctets\" ) / 1048576; my $output = $cust_pkg->attribute_since_sqlradacct($last_bill, $sdate, \"AcctOutputOctets\" ) / 1048576; my $total = $input + $output - \' + what.recur_included_total.value + \'; $total = 0 if $total < 0; my $input = $input - \' + what.recur_included_input.value + \'; $input = 0 if $input < 0; my $output = $output - \' + what.recur_included_output.value + \'; $output = 0 if $output < 0; my $totalcharge = sprintf(\"%.2f\", \' + what.recur_total_charge.value + \' * $total); my $hourscharge = sprintf(\"%.2f\", \' + what.recur_hourly_charge.value + \' * $hours); push @details, \"Last month\\\'s excess data \". sprintf(\"%.1f\", $total). \" megs: \\\$$totalcharge\", \"Last month\\\'s excess time \". sprintf(\"%.1f\", $hours). \" hours: \\\$$hourscharge\"; \' + what.recur_flat.value + \' + $hourscharge + \' + what.recur_input_charge.value + \' * $input + \' + what.recur_output_charge.value + \' * $output + $totalcharge ;\'',
+  },
+
+;
+
+my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
+                    split("\n", $part_pkg->plandata );
+
+tie my %options, 'Tie::IxHash', map { $_=>$plans{$_}->{'name'} } keys %plans;
+
+my @form_select = ();
+if ( $conf->exists('enable_taxclasses') ) {
+  push @form_select, 'taxclass';
+} else {
+  push @fixups, 'taxclass'; #hidden
+}
+
+
+my $widget = new HTML::Widgets::SelectLayers(
+  'selected_layer' => $part_pkg->plan,
+  'options'        => \%options,
+  'form_name'      => 'dummy',
+  'form_action'    => 'process/part_pkg.cgi',
+  'form_text'      => [ qw(pkg comment freq clone pkgnum pkgpart), @fixups ],
+  'form_checkbox'  => [ qw(setuptax recurtax disabled) ],
+  'form_select'    => [ @form_select ],
+  'fixup_callback' => sub {
+                        #my $ = @_;
+                        my $html = '';
+                        for my $p ( keys %plans ) {
+                          $html .= "if ( what.plan.value == \"$p\" ) {
+                                      what.setup.value = $plans{$p}->{setup} ;
+                                      what.recur.value = $plans{$p}->{recur} ;
+                                    }\n";
+                        }
+                        $html;
+                      },
+  'layer_callback' => sub {
+    my $layer = shift;
+    my $html = qq!<INPUT TYPE="hidden" NAME="plan" VALUE="$layer">!.
+               ntable("#cccccc",2);
+    my $href = $plans{$layer}->{'fields'};
+    foreach my $field ( exists($plans{$layer}->{'fieldorder'})
+                          ? @{$plans{$layer}->{'fieldorder'}}
+                          : keys %{ $href }
+                      ) {
+
+      $html .= '<TR><TD ALIGN="right">'. $href->{$field}{'name'}. '</TD><TD>';
+
+      if ( ! exists($href->{$field}{'type'}) ) {
+        $html .= qq!<INPUT TYPE="text" NAME="$field" VALUE="!.
+                 ( exists($plandata{$field})
+                     ? $plandata{$field}
+                     : $href->{$field}{'default'} ).
+                 qq!" onChange="fchanged(this)">!;
+      } elsif ( $href->{$field}{'type'} eq 'select_multiple' ) {
+        $html .= qq!<SELECT MULTIPLE NAME="$field" onChange="fchanged(this)">!;
+        foreach my $record (
+          qsearch( $href->{$field}{'select_table'},
+                   $href->{$field}{'select_hash'}   )
+        ) {
+          my $value = $record->getfield($href->{$field}{'select_key'});
+          $html .= qq!<OPTION VALUE="$value"!.
+                   (  $plandata{$field} =~ /(^|, *)$value *(,|$)/
+                        ? ' SELECTED'
+                        : ''          ).
+                   '>'. $record->getfield($href->{$field}{'select_label'})
+        }
+        $html .= '</SELECT>';
+      }
+
+      $html .= '</TD></TR>';
+    }
+    $html .= '</TABLE>';
+
+    $html .= '<INPUT TYPE="hidden" NAME="plandata" VALUE="'.
+             join(',', keys %{ $href } ). '">'.
+             '<BR><BR>';
+             
+    $html .= '<INPUT TYPE="submit" VALUE="'.
+             ( $hashref->{pkgpart} ? "Apply changes" : "Add package" ).
+             '" onClick="fchanged(this)">';
+
+    $html .= '<BR><BR>don\'t edit this unless you know what you\'re doing '.
+             '<INPUT TYPE="button" VALUE="refresh expressions" '.
+               'onClick="fchanged(this)">'.
+             ntable("#cccccc",2).
+             '<TR><TD>'.
+             '<FONT SIZE="1">Setup expression<BR>'.
+             '<INPUT TYPE="text" NAME="setup" SIZE="160" VALUE="'.
+               encode_entities($hashref->{setup}). '" onLoad="fchanged(this)">'.
+             '</FONT><BR>'.
+             '<FONT SIZE="1">Recurring espression<BR>'.
+             '<INPUT TYPE="text" NAME="recur" SIZE="160" VALUE="'.
+               encode_entities($hashref->{recur}). '" onLoad="fchanged(this)">'.
+             '</FONT>'.
+             '</TR></TD>'.
+             '</TABLE>';
+
+    $html;
+
+  },
+);
+
+%>
+
+<BR>
+Price plan <%= $widget->html %>
+  </BODY>
+</HTML>
diff --git a/httemplate/edit/part_referral.cgi b/httemplate/edit/part_referral.cgi
new file mode 100755 (executable)
index 0000000..f784dfa
--- /dev/null
@@ -0,0 +1,48 @@
+<!-- mason kludge -->
+<%
+
+my $part_referral;
+if ( $cgi->param('error') ) {
+  $part_referral = new FS::part_referral ( {
+    map { $_, scalar($cgi->param($_)) } fields('part_referral')
+  } );
+} elsif ( $cgi->keywords ) {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $part_referral = qsearchs( 'part_referral', { 'refnum' => $1 } );
+} else { #adding
+  $part_referral = new FS::part_referral {};
+}
+my $action = $part_referral->refnum ? 'Edit' : 'Add';
+my $hashref = $part_referral->hashref;
+
+my $p1 = popurl(1);
+print header("$action Advertising source", menubar(
+  'Main Menu' => popurl(2),
+  'View all advertising sources' => popurl(2). "browse/part_referral.cgi",
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/part_referral.cgi" METHOD=POST>!;
+
+print qq!<INPUT TYPE="hidden" NAME="refnum" VALUE="$hashref->{refnum}">!;
+#print "Referral #", $hashref->{refnum} ? $hashref->{refnum} : "(NEW)";
+
+print <<END;
+Advertising source <INPUT TYPE="text" NAME="referral" SIZE=32 VALUE="$hashref->{referral}">
+END
+
+print qq!<BR><INPUT TYPE="submit" VALUE="!,
+      $hashref->{refnum} ? "Apply changes" : "Add advertising source",
+      qq!">!;
+
+print <<END;
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/part_router_field.cgi b/httemplate/edit/part_router_field.cgi
new file mode 100644 (file)
index 0000000..02962b1
--- /dev/null
@@ -0,0 +1,71 @@
+<!-- mason kludge -->
+<%
+my ($routerfieldpart, $part_router_field);
+
+if ( $cgi->param('error') ) {
+  $part_router_field = new FS::part_router_field ( {
+    map { $_, scalar($cgi->param($_)) } fields('part_router_field')});
+  $routerfieldpart = $part_router_field->routerfieldpart;
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $routerfieldpart=$1;
+    $part_router_field=qsearchs('part_router_field',
+        {'routerfieldpart' => $routerfieldpart})
+      or die "Unknown routerfieldpart!";
+  
+  } else { #adding
+    $part_router_field = new FS::part_router_field({});
+  }
+}
+my $action = $part_router_field->routerfieldpart ? 'Edit' : 'Add';
+
+my $p1 = popurl(1);
+print header("$action Router Extended Field Definition",
+             menubar('Main Menu' => $p,
+                     'View all Extended Fields' => $p. 'browse/generic.cgi?part_router_field')
+            );
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+%>
+<FORM ACTION="<%=$p1%>process/generic.cgi" METHOD=POST>
+
+<INPUT TYPE="hidden" NAME="table" VALUE="part_router_field">
+<INPUT TYPE="hidden" NAME="routerfieldpart" VALUE="<%=
+  $routerfieldpart%>">
+Field #<B><%=$routerfieldpart or "(NEW)"%></B><BR><BR>
+
+<%=ntable("#cccccc",2)%>
+  <TR>
+    <TD ALIGN="right">Name</TD>
+    <TD><INPUT TYPE="text" NAME="name" MAXLENGTH=15 VALUE="<%=
+    $part_router_field->name%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Length</TD>
+    <TD><INPUT TYPE="text" NAME="length" MAXLENGTH=4 VALUE="<%=
+    $part_router_field->length%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">check_block</TD>
+    <TD><TEXTAREA COLS="20" ROWS="4" NAME="check_block"><%=
+    $part_router_field->check_block%></TEXTAREA></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">list_source</TD>
+    <TD><TEXTAREA COLS="20" ROWS="4" NAME="list_source"><%=
+    $part_router_field->list_source%></TEXTAREA></TD>
+  </TR>
+</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">
+
+</FORM>
+
+<BR><BR>
+<FONT SIZE=-2>If you don't understand what <I>check_block</I> and 
+<I>list_source</I> mean, <B>LEAVE THEM BLANK</B>.  We mean it.</FONT>
+
+
+</BODY>
+</HTML>
diff --git a/httemplate/edit/part_sb_field.cgi b/httemplate/edit/part_sb_field.cgi
new file mode 100644 (file)
index 0000000..9e0cc9e
--- /dev/null
@@ -0,0 +1,79 @@
+<!-- mason kludge -->
+<%
+my ($sbfieldpart, $part_sb_field);
+
+if ( $cgi->param('error') ) {
+  $part_sb_field = new FS::part_sb_field ( {
+    map { $_, scalar($cgi->param($_)) } fields('part_sb_field')});
+  $sbfieldpart = $part_sb_field->sbfieldpart;
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $sbfieldpart=$1;
+    $part_sb_field=qsearchs('part_sb_field',
+        {'sbfieldpart' => $sbfieldpart})
+      or die "Unknown sbfieldpart!";
+  
+  } else { #adding
+    $part_sb_field = new FS::part_sb_field({});
+  }
+}
+my $action = $part_sb_field->sbfieldpart ? 'Edit' : 'Add';
+
+my $p1 = popurl(1);
+print header("$action svc_broadband Extended Field Definition", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+%>
+<FORM ACTION="<%=$p1%>process/generic.cgi" METHOD="POST">
+
+<INPUT TYPE="hidden" NAME="table" VALUE="part_sb_field">
+<INPUT TYPE="hidden" NAME="redirect_ok" 
+    VALUE="<%=popurl(2)%>browse/part_sb_field.cgi">
+<INPUT TYPE="hidden" NAME="sbfieldpart" VALUE="<%=
+  $sbfieldpart%>">
+Field #<B><%=$sbfieldpart or "(NEW)"%></B><BR><BR>
+
+<%=ntable("#cccccc",2)%>
+  <TR>
+    <TD ALIGN="right">Name</TD>
+    <TD><INPUT TYPE="text" NAME="name" MAXLENGTH=15 VALUE="<%=
+    $part_sb_field->name%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Length</TD>
+    <TD><INPUT TYPE="text" NAME="length" MAXLENGTH=4 VALUE="<%=
+    $part_sb_field->length%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Service</TD>
+    <TD><SELECT SIZE=1 NAME="svcpart"><%
+      foreach my $part_svc (qsearch('part_svc', {svcdb => 'svc_broadband'})) {
+        %><OPTION VALUE="<%=$part_svc->svcpart%>"<%=
+         ($part_svc->svcpart == $part_sb_field->svcpart) ? ' SELECTED' : ''%>">
+         <%=$part_svc->svc%>
+      <% } %>
+      </SELECT></TD>
+  <TR>
+    <TD ALIGN="right">check_block</TD>
+    <TD><TEXTAREA COLS="20" ROWS="4" NAME="check_block"><%=
+    $part_sb_field->check_block%></TEXTAREA></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">list_source</TD>
+    <TD><TEXTAREA COLS="20" ROWS="4" NAME="list_source"><%=
+    $part_sb_field->list_source%></TEXTAREA></TD>
+  </TR>
+</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">
+
+</FORM>
+
+<BR><BR>
+<FONT SIZE=-2>If you don't understand what <I>check_block</I> and 
+<I>list_source</I> mean, <B>LEAVE THEM BLANK</B>.  We mean it.</FONT>
+
+
+</BODY>
+</HTML>
diff --git a/httemplate/edit/part_svc.cgi b/httemplate/edit/part_svc.cgi
new file mode 100755 (executable)
index 0000000..d4bb470
--- /dev/null
@@ -0,0 +1,249 @@
+<!-- mason kludge -->
+<% 
+   my $part_svc;
+   my $clone = '';
+   if ( $cgi->param('error') ) { #error
+     $part_svc = new FS::part_svc ( {
+       map { $_, scalar($cgi->param($_)) } fields('part_svc')
+     } );
+   } elsif ( $cgi->param('clone') && $cgi->param('clone') =~ /^(\d+)$/ ) {#clone
+     #$cgi->param('clone') =~ /^(\d+)$/ or die "malformed query: $query";
+     $part_svc = qsearchs('part_svc', { 'svcpart'=>$1 } )
+       or die "unknown svcpart: $1";
+     $clone = $part_svc->svcpart;
+     $part_svc->svcpart('');
+   } elsif ( $cgi->keywords ) { #edit
+     my($query) = $cgi->keywords;
+     $query =~ /^(\d+)$/ or die "malformed query: $query";
+     $part_svc=qsearchs('part_svc', { 'svcpart'=>$1 } )
+       or die "unknown svcpart: $1";
+   } else { #adding
+     $part_svc = new FS::part_svc {};
+   }
+   my $action = $part_svc->svcpart ? 'Edit' : 'Add';
+   my $hashref = $part_svc->hashref;
+#   my $p_svcdb = $part_svc->svcdb || 'svc_acct';
+
+
+           #" onLoad=\"visualize()\""
+%>
+
+<%= header("$action Service Definition",
+           menubar( 'Main Menu'         => $p,
+                    'View all service definitions' => "${p}browse/part_svc.cgi"
+                  ),
+           )
+%>
+
+<% if ( $cgi->param('error') ) { %>
+<FONT SIZE="+1" COLOR="#ff0000">Error: <%= $cgi->param('error') %></FONT>
+<% } %>
+
+<FORM NAME="dummy">
+
+      Service Part #<%= $part_svc->svcpart ? $part_svc->svcpart : "(NEW)" %>
+<BR><BR>
+Service  <INPUT TYPE="text" NAME="svc" VALUE="<%= $hashref->{svc} %>"><BR>
+Disable new orders <INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<%= $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>><BR>
+<INPUT TYPE="hidden" NAME="svcpart" VALUE="<%= $hashref->{svcpart} %>">
+<BR>
+Services are items you offer to your customers.
+<UL><LI>svc_acct - Shell accounts, POP mailboxes, SLIP/PPP and ISDN accounts
+    <LI>svc_domain - Domains
+    <LI>svc_forward - mail forwarding
+    <LI>svc_www - Virtual domain website
+    <LI>svc_broadband - Broadband/High-speed Internet service
+<!--   <LI>svc_charge - One-time charges (Partially unimplemented)
+       <LI>svc_wo - Work orders (Partially unimplemented)
+-->
+</UL>
+For the selected table, you can give fields default or fixed (unchangable)
+values.  For example, a SLIP/PPP account may have a default (or perhaps fixed)
+<B>slipip</B> of <B>0.0.0.0</B>, while a POP mailbox will probably have a fixed
+blank <B>slipip</B> as well as a fixed shell something like <B>/bin/true</B> or
+<B>/usr/bin/passwd</B>.
+<BR><BR>
+
+<%
+#these might belong somewhere else for other user interfaces 
+#pry need to eventually create stuff that's shared amount UIs
+my %defs = (
+  'svc_acct' => {
+    'dir'       => 'Home directory',
+    'uid'       => 'UID (set to fixed and blank for dial-only)',
+    'slipip'    => 'IP address (Set to fixed and blank to disable dialin, or, set a value to be exported to RADIUS Framed-IP-Address.  Use the special value <code>0e0</code> [zero e zero] to enable export to RADIUS without a Framed-IP-Address.)',
+#    'popnum'    => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
+    'popnum'    => {
+                     desc => 'Access number',
+                     type => 'select',
+                     select_table => 'svc_acct_pop',
+                     select_key   => 'popnum',
+                     select_label => 'city',
+                   },
+    'username'  => {
+                      desc => 'Username',
+                      type => 'disabled',
+                   },
+    'quota'     => '',
+    '_password' => 'Password',
+    'gid'       => 'GID (when blank, defaults to UID)',
+    'shell'     => 'Shell (all service definitions should have a default or fixed shell that is present in the <b>shells</b> configuration file)',
+    'finger'    => 'GECOS',
+    'domsvc'    => {
+                     desc =>'svcnum from svc_domain',
+                     type =>'select',
+                     select_table => 'svc_domain',
+                     select_key   => 'svcnum',
+                     select_label => 'domain',
+                   },
+    'usergroup' => {
+                     desc =>'ICRADIUS/FreeRADIUS groups',
+                     type =>'radius_usergroup_selector',
+                   },
+  },
+  'svc_domain' => {
+    'domain'    => 'Domain',
+  },
+  'svc_forward' => {
+    'srcsvc'    => 'service from which mail is to be forwarded',
+    'dstsvc'    => 'service to which mail is to be forwarded',
+    'dst'       => 'someone@another.domain.com to use when dstsvc is 0',
+  },
+  'svc_charge' => {
+    'amount'    => 'amount',
+  },
+  'svc_wo' => {
+    'worker'    => 'Worker',
+    '_date'      => 'Date',
+  },
+  'svc_www' => {
+    #'recnum' => '',
+    #'usersvc' => '',
+  },
+  'svc_broadband' => {
+    'actypenum' => 'This is the actypenum that refers to the type of AC that can be provisioned for this service.  This field must be set fixed.',
+    'speed_down' => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
+    'speed_up' => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
+    'acnum' => 'acnum of a specific AC that this service is restricted to.  Not required',
+    'ip_addr' => 'IP address.  Leave blank for automatic assignment.',
+    'ip_netmask' => 'Mask length, aka. netmask bits.  (Eg. 255.255.255.0 == 24)',
+    'mac_addr' => 'MAC address which is used by some ACs for access control.  Specified by 6 colon seperated hex octets. (Eg. 00:00:0a:bc:1a:2b)',
+    'location' => 'Defines the physically location at which this service was installed.  This is not necessarily the billing address',
+  },
+);
+
+  my @dbs = $hashref->{svcdb}
+             ? ( $hashref->{svcdb} )
+             : qw( svc_acct svc_domain svc_forward svc_www svc_broadband );
+
+  tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } @dbs;
+  my $widget = new HTML::Widgets::SelectLayers(
+    #'selected_layer' => $p_svcdb,
+    'selected_layer' => $hashref->{svcdb} || 'svc_acct',
+    'options'        => \%svcdb,
+    'form_name'      => 'dummy',
+    'form_action'    => 'process/part_svc.cgi',
+    'form_text'      => [ qw( svc svcpart ) ],
+    'form_checkbox'  => [ 'disabled' ],
+    'layer_callback' => sub {
+      my $layer = shift;
+      my $html = qq!<INPUT TYPE="hidden" NAME="svcdb" VALUE="$layer">!;
+
+      my $columns = 3;
+      my $count = 0;
+      my @part_export =
+        map { qsearch( 'part_export', {exporttype => $_ } ) }
+          keys %{FS::part_export::export_info($layer)};
+      $html .= '<BR><BR>'. table().
+               table(). "<TR><TH COLSPAN=$columns>Exports</TH></TR><TR>";
+      foreach my $part_export ( @part_export ) {
+        $html .= '<TD><INPUT TYPE="checkbox"'.
+                 ' NAME="exportnum'. $part_export->exportnum. '"  VALUE="1" ';
+        $html .= 'CHECKED'
+          if ( $clone || $part_svc->svcpart ) #null svcpart search causing error
+              && qsearchs( 'export_svc', {
+                                   exportnum => $part_export->exportnum,
+                                   svcpart   => $clone || $part_svc->svcpart });
+        $html .= '>'. $part_export->exportnum. ': '. $part_export->exporttype.
+                 ' to '. $part_export->machine. '</TD>';
+        $count++;
+        $html .= '</TR><TR>' unless $count % $columns;
+      }
+      $html .= '</TR></TABLE><BR><BR>';
+
+      $html .=  table(). "<TH>Field</TH><TH COLSPAN=2>Modifier</TH>";
+      #yucky kludge
+      my @fields = defined( $FS::Record::dbdef->table($layer) )
+                      ? grep { $_ ne 'svcnum' } fields($layer)
+                      : ();
+      push @fields, 'usergroup' if $layer eq 'svc_acct'; #kludge
+      $part_svc->svcpart($clone) if $clone; #haha, undone below
+      foreach my $field (@fields) {
+        my $part_svc_column = $part_svc->part_svc_column($field);
+        my $value = $cgi->param('error')
+                      ? $cgi->param("${layer}__${field}")
+                      : $part_svc_column->columnvalue;
+        my $flag = $cgi->param('error')
+                     ? $cgi->param("${layer}__${field}_flag")
+                     : $part_svc_column->columnflag;
+        my $def = $defs{$layer}{$field};
+        my $desc = ref($def) ? $def->{desc} : $def;
+        
+        $html .= "<TR><TD>$field";
+        $html .= "- <FONT SIZE=-1>$desc</FONT>" if $desc;
+        $html .=  "</TD>";
+        $flag = '' if ref($def) && $def->{type} eq 'disabled';
+        $html .=
+          qq!<TD><INPUT TYPE="radio" NAME="${layer}__${field}_flag" VALUE=""!.
+          ' CHECKED'x($flag eq ''). ">Off</TD>".
+          '<TD>';
+        unless ( ref($def) && $def->{type} eq 'disabled' ) {
+          $html .= 
+            qq!<INPUT TYPE="radio" NAME="${layer}__${field}_flag" VALUE="D"!.
+            ' CHECKED'x($flag eq 'D'). ">Default ".
+            qq!<INPUT TYPE="radio" NAME="${layer}__${field}_flag" VALUE="F"!.
+            ' CHECKED'x($flag eq 'F'). ">Fixed ".
+            '<BR>';
+        }
+        if ( ref($def) ) {
+          if ( $def->{type} eq 'select' ) {
+            $html .= qq!<SELECT NAME="${layer}__${field}">!;
+            $html .= '<OPTION> </OPTION>' unless $value;
+            foreach my $record ( qsearch( $def->{select_table}, {} ) ) {
+              my $rvalue = $record->getfield($def->{select_key});
+              $html .= qq!<OPTION VALUE="$rvalue"!.
+                       ( $rvalue==$value ? ' SELECTED>' : '>' ).
+                       $record->getfield($def->{select_label}). '</OPTION>';
+            }
+            $html .= '</SELECT>';
+          } elsif ( $def->{type} eq 'radius_usergroup_selector' ) {
+            $html .= FS::svc_acct::radius_usergroup_selector(
+              [ split(',', $value) ], "${layer}__${field}" );
+          } elsif ( $def->{type} eq 'disabled' ) {
+            $html .=
+              qq!<INPUT TYPE="hidden" NAME="${layer}__${field}" VALUE="">!;
+          } else {
+            $html .= '<font color="#ff0000">unknown type'. $def->{type};
+          }
+        } else {
+          $html .=
+            qq!<INPUT TYPE="text" NAME="${layer}__${field}" VALUE="$value">!;
+        }
+        $html .= "</TD></TR>\n";
+      }
+      $part_svc->svcpart('') if $clone; #undone
+      $html .= "</TABLE>";
+
+      $html .= '<BR><INPUT TYPE="submit" VALUE="'.
+               ($hashref->{svcpart} ? 'Apply changes' : 'Add service'). '">';
+
+      $html;
+
+    },
+  );
+
+%>
+Table <%= $widget->html %>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/edit/process/REAL_cust_pkg.cgi b/httemplate/edit/process/REAL_cust_pkg.cgi
new file mode 100755 (executable)
index 0000000..7f5c5e4
--- /dev/null
@@ -0,0 +1,22 @@
+<%
+
+my $pkgnum = $cgi->param('pkgnum') or die;
+my $old = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+my %hash = $old->hash;
+$hash{'setup'} = $cgi->param('setup') ? str2time($cgi->param('setup')) : '';
+$hash{'bill'} = $cgi->param('bill') ? str2time($cgi->param('bill')) : '';
+$hash{'last_bill'} =
+  $cgi->param('last_bill') ? str2time($cgi->param('last_bill')) : '';
+$hash{'expire'} = $cgi->param('expire') ? str2time($cgi->param('expire')) : '';
+my $new = new FS::cust_pkg \%hash;
+
+my $error = $new->replace($old);
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "REAL_cust_pkg.cgi?". $cgi->query_string );
+} else { 
+  print $cgi->redirect(popurl(3). "view/cust_pkg.cgi?". $pkgnum);
+}
+
+%>
diff --git a/httemplate/edit/process/addr_block/add.cgi b/httemplate/edit/process/addr_block/add.cgi
new file mode 100755 (executable)
index 0000000..34d799c
--- /dev/null
@@ -0,0 +1,20 @@
+<%
+
+my $error = '';
+my $ip_gateway = $cgi->param('ip_gateway');
+my $ip_netmask = $cgi->param('ip_netmask');
+
+my $new = new FS::addr_block {
+    ip_gateway => $ip_gateway,
+    ip_netmask => $ip_netmask,
+    routernum  => 0 };
+
+$error = $new->insert;
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi?". $cgi->query_string );
+} else { 
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+} 
+%>
diff --git a/httemplate/edit/process/addr_block/allocate.cgi b/httemplate/edit/process/addr_block/allocate.cgi
new file mode 100755 (executable)
index 0000000..85b0d7a
--- /dev/null
@@ -0,0 +1,25 @@
+<%
+my $error = '';
+my $blocknum = $cgi->param('blocknum');
+my $routernum = $cgi->param('routernum');
+
+my $addr_block = qsearchs('addr_block', { blocknum => $blocknum });
+my $router = qsearchs('router', { routernum => $routernum });
+
+if($addr_block) {
+  if ($router) {
+    $error = $addr_block->allocate($router);
+  } else {
+    $error = "Cannot find router with routernum $routernum";
+  }
+} else {
+  $error = "Cannot find block with blocknum $blocknum";
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi?" . $cgi->query_string);
+} else { 
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+}
+%>
diff --git a/httemplate/edit/process/addr_block/deallocate.cgi b/httemplate/edit/process/addr_block/deallocate.cgi
new file mode 100755 (executable)
index 0000000..cfb7ed0
--- /dev/null
@@ -0,0 +1,24 @@
+<%
+my $error = '';
+my $blocknum = $cgi->param('blocknum');
+
+my $addr_block = qsearchs('addr_block', { blocknum => $blocknum });
+
+if($addr_block) {
+  my $router = $addr_block->router;
+  if ($router) {
+    $error = $addr_block->deallocate($router);
+  } else {
+    $error = "Block is not allocated to a router";
+  }
+} else {
+  $error = "Cannot find block with blocknum $blocknum";
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi?" . $cgi->query_string);
+} else { 
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+}
+%>
diff --git a/httemplate/edit/process/addr_block/split.cgi b/httemplate/edit/process/addr_block/split.cgi
new file mode 100755 (executable)
index 0000000..bb6d4ba
--- /dev/null
@@ -0,0 +1,19 @@
+<%
+my $error = '';
+my $blocknum = $cgi->param('blocknum');
+my $addr_block = qsearchs('addr_block', { blocknum => $blocknum });
+
+if ( $addr_block) {
+  $error = $addr_block->split_block;
+} else {
+  $error = "Unknown blocknum: $blocknum";
+}
+
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi?". $cgi->query_string );
+} else { 
+  print $cgi->redirect(popurl(4). "browse/addr_block.cgi");
+} 
+%>
diff --git a/httemplate/edit/process/agent.cgi b/httemplate/edit/process/agent.cgi
new file mode 100755 (executable)
index 0000000..182eeab
--- /dev/null
@@ -0,0 +1,28 @@
+<%
+
+my $agentnum = $cgi->param('agentnum');
+
+my $old = qsearchs('agent',{'agentnum'=>$agentnum}) if $agentnum;
+
+my $new = new FS::agent ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('agent')
+} );
+
+my $error;
+if ( $agentnum ) {
+  $error=$new->replace($old);
+} else {
+  $error=$new->insert;
+  $agentnum=$new->getfield('agentnum');
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "agent.cgi?". $cgi->query_string );
+} else { 
+  print $cgi->redirect(popurl(3). "browse/agent.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/agent_type.cgi b/httemplate/edit/process/agent_type.cgi
new file mode 100755 (executable)
index 0000000..5165945
--- /dev/null
@@ -0,0 +1,55 @@
+<%
+
+my $typenum = $cgi->param('typenum');
+my $old = qsearchs('agent_type',{'typenum'=>$typenum}) if $typenum;
+
+my $new = new FS::agent_type ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('agent_type')
+} );
+
+my $error;
+if ( $typenum ) {
+  $error=$new->replace($old);
+} else {
+  $error=$new->insert;
+  $typenum=$new->getfield('typenum');
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "agent_type.cgi?". $cgi->query_string );
+} else {
+
+  #false laziness w/ edit/process/part_svc.cgi
+  foreach my $part_pkg (qsearch('part_pkg',{})) {
+    my($pkgpart)=$part_pkg->getfield('pkgpart');
+
+    my($type_pkgs)=qsearchs('type_pkgs',{
+        'typenum' => $typenum,
+        'pkgpart' => $pkgpart,
+    });
+    if ( $type_pkgs && ! $cgi->param("pkgpart$pkgpart") ) {
+      my($d_type_pkgs)=$type_pkgs; #need to save $type_pkgs for below.
+      $error=$d_type_pkgs->delete;
+      die $error if $error;
+
+    } elsif ( $cgi->param("pkgpart$pkgpart")
+              && ! $type_pkgs
+    ) {
+      #ok to clobber it now (but bad form nonetheless?)
+      $type_pkgs=new FS::type_pkgs ({
+        'typenum' => $typenum,
+        'pkgpart' => $pkgpart,
+      });
+      $error= $type_pkgs->insert;
+      die $error if $error;
+    }
+
+  }
+
+  print $cgi->redirect(popurl(3). "browse/agent_type.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/cust_bill_pay.cgi b/httemplate/edit/process/cust_bill_pay.cgi
new file mode 100755 (executable)
index 0000000..0c33506
--- /dev/null
@@ -0,0 +1,31 @@
+<%
+
+$cgi->param('paynum') =~ /^(\d*)$/ or die "Illegal paynum!";
+my $paynum = $1;
+
+my $cust_pay = qsearchs('cust_pay', { 'paynum' => $paynum } )
+  or die "No such paynum";
+
+my $cust_main = qsearchs('cust_main', { 'custnum' => $cust_pay->custnum } )
+  or die "Bogus credit:  not attached to customer";
+
+my $custnum = $cust_main->custnum;
+
+my $new = new FS::cust_bill_pay ( {
+  map {
+    $_, scalar($cgi->param($_));
+  #} qw(custnum _date amount invnum)
+  } fields('cust_bill_pay')
+} );
+
+my $error = $new->insert;
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "cust_bill_pay.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+}
+
+
+%>
diff --git a/httemplate/edit/process/cust_credit.cgi b/httemplate/edit/process/cust_credit.cgi
new file mode 100755 (executable)
index 0000000..ac92631
--- /dev/null
@@ -0,0 +1,30 @@
+<%
+
+$cgi->param('custnum') =~ /^(\d*)$/ or die "Illegal custnum!";
+my $custnum = $1;
+
+$cgi->param('otaker',getotaker);
+
+my $new = new FS::cust_credit ( {
+  map {
+    $_, scalar($cgi->param($_));
+  #} qw(custnum _date amount otaker reason)
+  } fields('cust_credit')
+} );
+
+my $error = $new->insert;
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "cust_credit.cgi?". $cgi->query_string );
+} else {
+  if ( $cgi->param('apply') eq 'yes' ) {
+    my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum })
+      or die "unknown custnum $custnum";
+    $cust_main->apply_credits;
+  }
+  print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+}
+
+
+%>
diff --git a/httemplate/edit/process/cust_credit_bill.cgi b/httemplate/edit/process/cust_credit_bill.cgi
new file mode 100755 (executable)
index 0000000..23e2e6c
--- /dev/null
@@ -0,0 +1,43 @@
+<%
+
+$cgi->param('crednum') =~ /^(\d*)$/ or die "Illegal crednum!";
+my $crednum = $1;
+
+my $cust_credit = qsearchs('cust_credit', { 'crednum' => $crednum } )
+  or die "No such crednum";
+
+my $cust_main = qsearchs('cust_main', { 'custnum' => $cust_credit->custnum } )
+  or die "Bogus credit:  not attached to customer";
+
+my $custnum = $cust_main->custnum;
+
+my $new;
+if ($cgi->param('invnum') =~ /^Refund$/) {
+  $new = new FS::cust_refund ( {
+    'reason'  => $cust_credit->reason,
+    'refund'  => $cgi->param('amount'),
+    'payby'   => 'BILL',
+    #'_date'   => $cgi->param('_date'),
+    'payinfo' => 'Cash',
+    'crednum' => $crednum,
+  } );
+} else {
+  $new = new FS::cust_credit_bill ( {
+    map {
+      $_, scalar($cgi->param($_));
+    #} qw(custnum _date amount invnum)
+    } fields('cust_credit_bill')
+  } );
+}
+
+my $error = $new->insert;
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "cust_credit_bill.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+}
+
+
+%>
diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi
new file mode 100755 (executable)
index 0000000..3700d9b
--- /dev/null
@@ -0,0 +1,125 @@
+<%
+
+my $error = '';
+
+#unmunge stuff
+
+$cgi->param('tax','') unless defined $cgi->param('tax');
+
+$cgi->param('refnum', (split(/:/, ($cgi->param('refnum'))[0] ))[0] );
+
+my $payby = $cgi->param('payby');
+if ( $payby ) {
+  if ( $payby eq 'CHEK' || $payby eq 'DCHK' ) {
+    $cgi->param('payinfo',
+      $cgi->param($payby. '_payinfo1'). '@'. $cgi->param($payby. '_payinfo2') );
+  } else {
+    $cgi->param('payinfo', $cgi->param( $payby. '_payinfo' ) );
+  }
+  $cgi->param('paydate',
+    $cgi->param( $payby. '_month' ). '-'. $cgi->param( $payby. '_year' ) );
+  $cgi->param('payname', $cgi->param( $payby. '_payname' ) );
+}
+
+$cgi->param('otaker', &getotaker );
+
+my @invoicing_list = split( /\s*\,\s*/, $cgi->param('invoicing_list') );
+push @invoicing_list, 'POST' if $cgi->param('invoicing_list_POST');
+$cgi->param('invoicing_list', join(',', @invoicing_list) );
+
+#create new record object
+
+my $new = new FS::cust_main ( {
+  map {
+    $_, scalar($cgi->param($_))
+#  } qw(custnum agentnum last first ss company address1 address2 city county
+#       state zip daytime night fax payby payinfo paydate payname tax
+#       otaker refnum)
+  } fields('cust_main')
+} );
+
+if ( defined($cgi->param('same')) && $cgi->param('same') eq "Y" ) {
+  $new->setfield("ship_$_", '') foreach qw(
+    last first company address1 address2 city county state zip
+    country daytime night fax
+  );
+}
+
+#perhaps this stuff should go to cust_main.pm
+my $cust_pkg = '';
+my $svc_acct = '';
+if ( $new->custnum eq '' ) {
+
+  if ( $cgi->param('pkgpart_svcpart') ) {
+    my $x = $cgi->param('pkgpart_svcpart');
+    $x =~ /^(\d+)_(\d+)$/;
+    my($pkgpart, $svcpart) = ($1, $2);
+    #false laziness: copied from FS::cust_pkg::order (which should become a
+    #FS::cust_main method)
+    my(%part_pkg);
+    # generate %part_pkg
+    # $part_pkg{$pkgpart} is true iff $custnum may purchase $pkgpart
+    my $agent = qsearchs('agent',{'agentnum'=> $new->agentnum });
+       #my($type_pkgs);
+       #foreach $type_pkgs ( qsearch('type_pkgs',{'typenum'=> $agent->typenum }) ) {
+       #  my($pkgpart)=$type_pkgs->pkgpart;
+       #  $part_pkg{$pkgpart}++;
+       #}
+    # $pkgpart_href->{PKGPART} is true iff $custnum may purchase $pkgpart
+    my $pkgpart_href = $agent->pkgpart_hashref;
+    #eslaf
+
+    # this should wind up in FS::cust_pkg!
+    $error ||= "Agent ". $new->agentnum. " (type ". $agent->typenum. ") can't".
+               "purchase pkgpart ". $pkgpart
+      #unless $part_pkg{ $pkgpart };
+      unless $pkgpart_href->{ $pkgpart };
+
+    $cust_pkg = new FS::cust_pkg ( {
+      #later         'custnum' => $custnum,
+      'pkgpart' => $pkgpart,
+    } );
+    $error ||= $cust_pkg->check;
+
+    #$cust_svc = new FS::cust_svc ( { 'svcpart' => $svcpart } );
+
+    #$error ||= $cust_svc->check;
+
+    $svc_acct = new FS::svc_acct ( {
+                                     'svcpart'   => $svcpart,
+                                     'username'  => $cgi->param('username'),
+                                     '_password' => $cgi->param('_password'),
+                                     'popnum'    => $cgi->param('popnum'),
+                                   } );
+
+    my $y = $svc_acct->setdefault; # arguably should be in new method
+    $error ||= $y unless ref($y);
+    #and just in case you were silly
+    $svc_acct->svcpart($svcpart);
+    $svc_acct->username($cgi->param('username'));
+    $svc_acct->_password($cgi->param('_password'));
+    $svc_acct->popnum($cgi->param('popnum'));
+
+    $error ||= $svc_acct->check;
+
+  } elsif ( $cgi->param('username') ) { #good thing to catch
+    $error = "Can't assign username without a package!";
+  }
+
+  use Tie::RefHash;
+  tie my %hash, 'Tie::RefHash';
+  %hash = ( $cust_pkg => [ $svc_acct ] ) if $cust_pkg;
+  $error ||= $new->insert( \%hash, \@invoicing_list );
+} else { #create old record object
+  my $old = qsearchs( 'cust_main', { 'custnum' => $new->custnum } ); 
+  $error ||= "Old record not found!" unless $old;
+  $error ||= $new->replace($old, \@invoicing_list);
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "cust_main.cgi?". $cgi->query_string );
+} else { 
+  print $cgi->redirect(popurl(3). "view/cust_main.cgi?". $new->custnum);
+} 
+%>
diff --git a/httemplate/edit/process/cust_main_county-collapse.cgi b/httemplate/edit/process/cust_main_county-collapse.cgi
new file mode 100755 (executable)
index 0000000..8e67140
--- /dev/null
@@ -0,0 +1,35 @@
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ or die "Illegal taxnum!";
+my $taxnum = $1;
+my $cust_main_county = qsearchs('cust_main_county',{'taxnum'=>$taxnum})
+  or die ("Unknown taxnum!");
+
+#really should do this in a .pm & start transaction
+
+foreach my $delete ( qsearch('cust_main_county', {
+                    'country' => $cust_main_county->country,
+                    'state' => $cust_main_county->state  
+                 } ) ) {
+#  unless ( qsearch('cust_main',{
+#    'state'  => $cust_main_county->getfield('state'),
+#    'county' => $cust_main_county->getfield('county'),
+#    'country' =>  $cust_main_county->getfield('country'),
+#  } ) ) {
+    my $error = $delete->delete;
+    die $error if $error;
+#  } else {
+    #should really fix the $cust_main record
+#  }
+
+}
+
+$cust_main_county->taxnum('');
+$cust_main_county->county('');
+my $error = $cust_main_county->insert;
+die $error if $error;
+
+print $cgi->redirect(popurl(3). "browse/cust_main_county.cgi");
+
+%>
diff --git a/httemplate/edit/process/cust_main_county-expand.cgi b/httemplate/edit/process/cust_main_county-expand.cgi
new file mode 100755 (executable)
index 0000000..a452711
--- /dev/null
@@ -0,0 +1,58 @@
+<%
+
+$cgi->param('taxnum') =~ /^(\d+)$/ or die "Illegal taxnum!";
+my $taxnum = $1;
+my $cust_main_county = qsearchs('cust_main_county',{'taxnum'=>$taxnum})
+  or die ("Unknown taxnum!");
+
+my @expansion;
+if ( $cgi->param('delim') eq 'n' ) {
+  @expansion=split(/\n/,$cgi->param('expansion'));
+} elsif ( $cgi->param('delim') eq 's' ) {
+  @expansion=split(' ',$cgi->param('expansion'));
+} else {
+  die "Illegal delim!";
+}
+
+@expansion=map {
+  unless ( /^\s*([\w\- ]+)\s*$/ ) {
+    $cgi->param('error', "Illegal item in expansion");
+    print $cgi->redirect(popurl(2). "cust_main_county-expand.cgi?". $cgi->query_string );
+    myexit();
+  }
+  $1;
+} @expansion;
+
+foreach ( @expansion) {
+  my(%hash)=$cust_main_county->hash;
+  my($new)=new FS::cust_main_county \%hash;
+  $new->setfield('taxnum','');
+  if ( $cgi->param('taxclass') ) {
+    $new->setfield('taxclass', $_);
+  } elsif ( ! $cust_main_county->state ) {
+    $new->setfield('state',$_);
+  } else {
+    $new->setfield('county',$_);
+  }
+  #if (datasrc =~ m/Pg/)
+  #{
+  #    $new->setfield('tax',0.0);
+  #}
+  my($error)=$new->insert;
+  die $error if $error;
+}
+
+unless ( qsearch( 'cust_main', {
+                                 'state'  => $cust_main_county->state,
+                                 'county' => $cust_main_county->county,
+                                 'country' =>  $cust_main_county->country,
+                               } )
+         || ! @expansion
+) {
+  my($error)=($cust_main_county->delete);
+  die $error if $error;
+}
+
+print $cgi->redirect(popurl(3). "browse/cust_main_county.cgi");
+
+%>
diff --git a/httemplate/edit/process/cust_main_county.cgi b/httemplate/edit/process/cust_main_county.cgi
new file mode 100755 (executable)
index 0000000..6d80ad5
--- /dev/null
@@ -0,0 +1,26 @@
+<%
+
+foreach ( grep { /^tax\d+$/ } $cgi->param ) {
+  /^tax(\d+)$/ or die "Illegal form $_!";
+  my $taxnum = $1;
+  my $old = qsearchs('cust_main_county', { 'taxnum' => $taxnum })
+    or die "Couldn't find taxnum $taxnum!";
+  next unless    $old->tax           != $cgi->param("tax$taxnum")
+              || $old->exempt_amount != $cgi->param("exempt_amount$taxnum")
+              || $old->taxname       ne $cgi->param("taxname$taxnum");
+  my %hash = $old->hash;
+  $hash{tax} = $cgi->param("tax$taxnum");
+  $hash{exempt_amount} = $cgi->param("exempt_amount$taxnum");
+  $hash{taxname} = $cgi->param("taxname$taxnum");
+  my $new = new FS::cust_main_county \%hash;
+  my $error = $new->replace($old);
+  if ( $error ) {
+    $cgi->param('error', $error);
+    print $cgi->redirect(popurl(2). "cust_main_county.cgi?". $cgi->query_string );
+    myexit();
+  }
+}
+
+print $cgi->redirect(popurl(3). "browse/cust_main_county.cgi");
+
+%>
diff --git a/httemplate/edit/process/cust_pay.cgi b/httemplate/edit/process/cust_pay.cgi
new file mode 100755 (executable)
index 0000000..82442ae
--- /dev/null
@@ -0,0 +1,39 @@
+<%
+
+$cgi->param('linknum') =~ /^(\d+)$/
+  or die "Illegal linknum: ". $cgi->param('linknum');
+my $linknum = $1;
+
+$cgi->param('link') =~ /^(custnum|invnum)$/
+  or die "Illegal link: ". $cgi->param('link');
+my $link = $1;
+
+my $new = new FS::cust_pay ( {
+  $link => $linknum,
+  map {
+    $_, scalar($cgi->param($_));
+  } qw(paid _date payby payinfo paybatch)
+  #} fields('cust_pay')
+} );
+
+my $error = $new->insert;
+
+if ($error) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). 'cust_pay.cgi?'. $cgi->query_string );
+} elsif ( $link eq 'invnum' ) {
+  print $cgi->redirect(popurl(3). "view/cust_bill.cgi?$linknum");
+} elsif ( $link eq 'custnum' ) {
+  if ( $cgi->param('apply') eq 'yes' ) {
+    my $cust_main = qsearchs('cust_main', { 'custnum' => $linknum })
+      or die "unknown custnum $linknum";
+    $cust_main->apply_payments;
+  }
+  if ( $cgi->param('quickpay') eq 'yes' ) {
+    print $cgi->redirect(popurl(3). "search/cust_main-quickpay.html");
+  } else {
+    print $cgi->redirect(popurl(3). "view/cust_main.cgi?$linknum");
+  }
+}
+
+%>
diff --git a/httemplate/edit/process/cust_pkg.cgi b/httemplate/edit/process/cust_pkg.cgi
new file mode 100755 (executable)
index 0000000..df8471c
--- /dev/null
@@ -0,0 +1,43 @@
+<%
+
+my $error = '';
+
+#untaint custnum
+$cgi->param('custnum') =~ /^(\d+)$/;
+my $custnum = $1;
+
+my @remove_pkgnums = map {
+  /^(\d+)$/ or die "Illegal remove_pkg value!";
+  $1;
+} $cgi->param('remove_pkg');
+
+my $error_redirect;
+my @pkgparts;
+if ( $cgi->param('new_pkgpart') =~ /^(\d+)$/ ) { #came from misc/change_pkg.cgi
+  $error_redirect = "misc/change_pkg.cgi";
+  @pkgparts = ($1);
+} else { #came from edit/cust_pkg.cgi
+  $error_redirect = "edit/cust_pkg.cgi";
+  foreach my $pkgpart ( map /^pkg(\d+)$/ ? $1 : (), $cgi->param ) {
+    if ( $cgi->param("pkg$pkgpart") =~ /^(\d+)$/ ) {
+      my $num_pkgs = $1;
+      while ( $num_pkgs-- ) {
+        push @pkgparts,$pkgpart;
+      }
+    } else {
+      $error = "Illegal quantity";
+      last;
+    }
+  }
+}
+
+$error ||= FS::cust_pkg::order($custnum,\@pkgparts,\@remove_pkgnums);
+
+if ($error) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(3). $error_redirect. '?'. $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum");
+}
+
+%>
diff --git a/httemplate/edit/process/domain_record.cgi b/httemplate/edit/process/domain_record.cgi
new file mode 100755 (executable)
index 0000000..b8c3f62
--- /dev/null
@@ -0,0 +1,34 @@
+<%
+
+my $recnum = $cgi->param('recnum');
+
+my $old = qsearchs('agent',{'recnum'=>$recnum}) if $recnum;
+
+my $new = new FS::domain_record ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('domain_record')
+} );
+
+my $error;
+if ( $recnum ) {
+  $error=$new->replace($old);
+} else {
+  $error=$new->insert;
+  $recnum=$new->getfield('recnum');
+}
+
+if ( $error ) {
+#  $cgi->param('error', $error);
+#  print $cgi->redirect(popurl(2). "agent.cgi?". $cgi->query_string );
+  #no edit screen to send them back to
+%>
+<!-- mason kludge -->
+<%
+  eidiot($error);
+} else { 
+  my $svcnum = $new->svcnum;
+  print $cgi->redirect(popurl(3). "view/svc_domain.cgi?$svcnum");
+}
+
+%>
diff --git a/httemplate/edit/process/generic.cgi b/httemplate/edit/process/generic.cgi
new file mode 100644 (file)
index 0000000..9c54feb
--- /dev/null
@@ -0,0 +1,70 @@
+<%
+
+# Welcome to generic.cgi.
+# 
+# This script provides a generic edit/process/ backend for simple table 
+# editing.  All it knows how to do is take the values entered into 
+# the script and insert them into the table specified by $cgi->param('table').
+# If there's an existing record with the same primary key, it will be 
+# replaced.  (Deletion will be added in the future.)
+# 
+# Special cgi params for this script:
+# table: the name of the table to be edited.  The script will die horribly 
+#        if it can't find the table.
+# redirect_ok: URL to be displayed after a successful edit.  The value of 
+#              the record's primary key will be passed as a keyword.
+#              Defaults to (freeside root)/view/$table.cgi.
+# redirect_error: URL to be displayed if there's an error.  The original 
+#                 query string, plus the error message, will be passed.
+#                 Defaults to $cgi->referer() (i.e. go back where you 
+#                 came from).
+
+
+use FS::Record qw(qsearchs dbdef);
+use DBIx::DBSchema;
+use DBIx::DBSchema::Table;
+
+
+my $error;
+my $p2 = popurl(2);
+my $p3 = popurl(3);
+my $table = $cgi->param('table');
+my $dbdef = dbdef or die "Cannot fetch dbdef!";
+
+my $dbdef_table = $dbdef->table($table) or die "Cannot fetch schema for $table";
+
+my $pkey = $dbdef_table->primary_key or die "Cannot fetch pkey for $table";
+my $pkey_val = $cgi->param($pkey);
+
+
+#warn "new FS::Record ( $table, (hashref) )";
+my $new = FS::Record::new ( "FS::$table", {
+    map { $_, scalar($cgi->param($_)) } fields($table) 
+} );
+
+#warn 'created $new of class '.ref($new);
+
+if($pkey_val and (my $old = qsearchs($table, { $pkey, $pkey_val} ))) {
+  # edit
+  $error = $new->replace($old);
+} else {
+  #add
+  $error = $new->insert;
+  $pkey_val = $new->getfield($pkey);
+  # New records usually don't have their primary keys set until after 
+  # they've been checked/inserted, so grab the new $pkey_val so we can 
+  # redirect to it.
+}
+
+my $redirect_ok = (($cgi->param('redirect_ok')) ?
+                    $cgi->param('redirect_ok') : $p3."browse/generic.cgi?$table");
+my $redirect_error = (($cgi->param('redirect_error')) ?
+                       $cgi->param('redirect_error') : $cgi->referer());
+
+if($error) {
+  $cgi->param('error', $error);
+  print $cgi->redirect($redirect_error . '?' . $cgi->query_string);
+} else {
+  print $cgi->redirect($redirect_ok);
+}
+%>
diff --git a/httemplate/edit/process/msgcat.cgi b/httemplate/edit/process/msgcat.cgi
new file mode 100644 (file)
index 0000000..1f94f66
--- /dev/null
@@ -0,0 +1,20 @@
+<%
+
+my $error;
+foreach my $param ( grep { /^\d+$/ } $cgi->param ) {
+  my $old = qsearchs('msgcat', { msgnum=>$param } );
+  next if $old->msg eq $cgi->param($param); #no need to update identical records
+  my $new = new FS::msgcat { $old->hash };
+  $new->msg($cgi->param($param));
+  $error = $new->replace($old);
+  last if $error;
+}
+
+if ( $error ) {
+  $cgi->param('error',$error);
+  print $cgi->redirect($p. "msgcat.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "browse/msgcat.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/part_bill_event.cgi b/httemplate/edit/process/part_bill_event.cgi
new file mode 100755 (executable)
index 0000000..e224bf6
--- /dev/null
@@ -0,0 +1,53 @@
+<%
+
+my $eventpart = $cgi->param('eventpart');
+
+my $old = qsearchs('part_bill_event',{'eventpart'=>$eventpart}) if $eventpart;
+
+#s/days/seconds/
+$cgi->param('seconds', $cgi->param('days') * 86400 );
+
+my $error;
+if ( ! $cgi->param('plan_weight_eventcode') ) {
+  $error = "Must select an action";
+} else {
+
+  $cgi->param('plan_weight_eventcode') =~ /^([\w\-]+):(\d+):(.*)$/s
+    or die "illegal plan_weight_eventcode:".
+           $cgi->param('plan_weight_eventcode');
+  $cgi->param('plan', $1);
+  $cgi->param('weight', $2);
+  my $eventcode = $3;
+  my $plandata = '';
+  while ( $eventcode =~ /%%%(\w+)%%%/ ) {
+    my $field = $1;
+    my $value = $cgi->param($field);
+    $eventcode =~ s/%%%$field%%%/$value/;
+    $plandata .= "$field $value\n";
+  }
+  $cgi->param('eventcode', $eventcode);
+  $cgi->param('plandata', $plandata);
+
+  my $new = new FS::part_bill_event ( {
+    map {
+      $_, scalar($cgi->param($_));
+    } fields('part_bill_event'),
+  } );
+
+  if ( $eventpart ) {
+    $error = $new->replace($old);
+  } else {
+    $error = $new->insert;
+    $eventpart = $new->getfield('eventpart');
+  }
+} 
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "part_bill_event.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3)."browse/part_bill_event.cgi");
+}
+
+%>
+
diff --git a/httemplate/edit/process/part_export.cgi b/httemplate/edit/process/part_export.cgi
new file mode 100644 (file)
index 0000000..fa009ed
--- /dev/null
@@ -0,0 +1,39 @@
+<%
+
+my $exportnum = $cgi->param('exportnum');
+
+my $old = qsearchs('part_export', { 'exportnum'=>$exportnum } ) if $exportnum;
+
+#fixup options
+#warn join('-', split(',',$cgi->param('options')));
+my %options = map {
+  my $value = $cgi->param($_);
+  $value =~ s/\r\n/\n/g; #browsers? (textarea)
+  $_ => $value;
+} split(',', $cgi->param('options'));
+
+my $new = new FS::part_export ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('part_export')
+} );
+
+my $error;
+if ( $exportnum ) {
+  #warn $old;
+  #warn $exportnum;
+  #warn $new->machine;
+  $error = $new->replace($old,\%options);
+} else {
+  $error = $new->insert(\%options);
+#  $exportnum = $new->exportnum;
+}
+
+if ( $error ) {
+  $cgi->param('error', $error );
+  print $cgi->redirect(popurl(2). "part_export.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "browse/part_export.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/part_pkg.cgi b/httemplate/edit/process/part_pkg.cgi
new file mode 100755 (executable)
index 0000000..d489426
--- /dev/null
@@ -0,0 +1,109 @@
+<%
+
+my $dbh = dbh;
+
+my $pkgpart = $cgi->param('pkgpart');
+
+my $old = qsearchs('part_pkg',{'pkgpart'=>$pkgpart}) if $pkgpart;
+
+#fixup plandata
+my $plandata = $cgi->param('plandata');
+my @plandata = split(',', $plandata);
+$cgi->param('plandata', 
+  join('', map { "$_=". join(', ', $cgi->param($_)). "\n" } @plandata )
+);
+
+foreach (qw( setuptax recurtax disabled )) {
+  $cgi->param($_, '') unless defined $cgi->param($_);
+}
+
+my $new = new FS::part_pkg ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('part_pkg')
+} );
+
+#warn "setuptax: ". $new->setuptax;
+#warn "recurtax: ". $new->recurtax;
+
+#most of the stuff below should move to part_pkg.pm
+
+foreach my $part_svc ( qsearch('part_svc', {} ) ) {
+  my $quantity = $cgi->param('pkg_svc'. $part_svc->svcpart) || 0;
+  unless ( $quantity =~ /^(\d+)$/ ) {
+    $cgi->param('error', "Illegal quantity" );
+    print $cgi->redirect(popurl(2). "part_pkg.cgi?". $cgi->query_string );
+    myexit();
+  }
+}
+
+local $SIG{HUP} = 'IGNORE';
+local $SIG{INT} = 'IGNORE';
+local $SIG{QUIT} = 'IGNORE';
+local $SIG{TERM} = 'IGNORE';
+local $SIG{TSTP} = 'IGNORE';
+local $SIG{PIPE} = 'IGNORE';
+
+local $FS::UID::AutoCommit = 0;
+
+my $error;
+if ( $pkgpart ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $pkgpart=$new->pkgpart;
+}
+if ( $error ) {
+  $dbh->rollback;
+  $cgi->param('error', $error );
+  print $cgi->redirect(popurl(2). "part_pkg.cgi?". $cgi->query_string );
+  myexit();
+}
+
+foreach my $part_svc (qsearch('part_svc',{})) {
+  my $quantity = $cgi->param('pkg_svc'. $part_svc->svcpart) || 0;
+  my $old_pkg_svc = qsearchs('pkg_svc', {
+    'pkgpart' => $pkgpart,
+    'svcpart' => $part_svc->svcpart,
+  } );
+  my $old_quantity = $old_pkg_svc ? $old_pkg_svc->quantity : 0;
+  next unless $old_quantity != $quantity; #!here
+  my $new_pkg_svc = new FS::pkg_svc( {
+    'pkgpart'  => $pkgpart,
+    'svcpart'  => $part_svc->svcpart,
+    'quantity' => $quantity, 
+  } );
+  if ( $old_pkg_svc ) {
+    my $myerror = $new_pkg_svc->replace($old_pkg_svc);
+    if ( $myerror ) {
+      $dbh->rollback;
+      die $myerror;
+    }
+  } else {
+    my $myerror = $new_pkg_svc->insert;
+    if ( $myerror ) {
+      $dbh->rollback;
+      die $myerror;
+    }
+  }
+}
+
+unless ( $cgi->param('pkgnum') && $cgi->param('pkgnum') =~ /^(\d+)$/ ) {
+  $dbh->commit or die $dbh->errstr;
+  print $cgi->redirect(popurl(3). "browse/part_pkg.cgi");
+} else {
+  my($old_cust_pkg) = qsearchs( 'cust_pkg', { 'pkgnum' => $1 } );
+  my %hash = $old_cust_pkg->hash;
+  $hash{'pkgpart'} = $pkgpart;
+  my($new_cust_pkg) = new FS::cust_pkg \%hash;
+  my $myerror = $new_cust_pkg->replace($old_cust_pkg);
+  if ( $myerror ) {
+    $dbh->rollback;
+    die "Error modifying cust_pkg record: $myerror\n";
+  }
+
+  $dbh->commit or die $dbh->errstr;
+  print $cgi->redirect(popurl(3). "view/cust_main.cgi?". $new_cust_pkg->custnum);
+}
+
+%>
diff --git a/httemplate/edit/process/part_referral.cgi b/httemplate/edit/process/part_referral.cgi
new file mode 100755 (executable)
index 0000000..fd2c015
--- /dev/null
@@ -0,0 +1,28 @@
+<%
+
+my $refnum = $cgi->param('refnum');
+
+my $new = new FS::part_referral ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('part_referral')
+} );
+
+my $error;
+if ( $refnum ) {
+  my $old = qsearchs( 'part_referral', { 'refnum' =>$ refnum } );
+  die "(Old) Record not found!" unless $old;
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+}
+$refnum=$new->refnum;
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "part_referral.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "browse/part_referral.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/part_svc.cgi b/httemplate/edit/process/part_svc.cgi
new file mode 100755 (executable)
index 0000000..9633fab
--- /dev/null
@@ -0,0 +1,62 @@
+<%
+
+my $svcpart = $cgi->param('svcpart');
+
+my $old = qsearchs('part_svc',{'svcpart'=>$svcpart}) if $svcpart;
+
+$cgi->param( 'svc_acct__usergroup',
+             join(',', $cgi->param('svc_acct__usergroup') ) );
+
+my $new = new FS::part_svc ( {
+  map {
+    $_, scalar($cgi->param($_));
+#  } qw(svcpart svc svcdb)
+  } ( fields('part_svc'),
+      map { my $svcdb = $_;
+            my @fields = fields($svcdb);
+            push @fields, 'usergroup' if $svcdb eq 'svc_acct'; #kludge
+            map { ( $svcdb.'__'.$_, $svcdb.'__'.$_.'_flag' )  } @fields;
+          } grep defined( $FS::Record::dbdef->table($_) ),
+                 qw( svc_acct svc_domain svc_forward svc_www svc_broadband )
+    )
+} );
+
+my $error;
+if ( $svcpart ) {
+  $error = $new->replace($old, '1.3-COMPAT', [ 'usergroup' ] );
+} else {
+  $error = $new->insert( [ 'usergroup' ] );
+  $svcpart=$new->getfield('svcpart');
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "part_svc.cgi?". $cgi->query_string );
+} else {
+
+  #false laziness w/ edit/process/agent_type.cgi
+  foreach my $part_export (qsearch('part_export',{})) {
+    my $exportnum = $part_export->exportnum;
+    my $export_svc = qsearchs('export_svc', {
+      'exportnum' => $part_export->exportnum,
+      'svcpart'   => $new->svcpart,
+    } );
+    if ( $export_svc && ! $cgi->param("exportnum". $part_export->exportnum) ) {
+      $error = $export_svc->delete;
+      die $error if $error;
+    } elsif ( $cgi->param("exportnum". $part_export->exportnum)
+              && ! $export_svc ) {
+      $export_svc = new FS::export_svc ( {
+        'exportnum' => $part_export->exportnum,
+        'svcpart'   => $new->svcpart,
+      } );
+      $error = $export_svc->insert;
+      die $error if $error;
+    }
+
+  }
+
+  print $cgi->redirect(popurl(3)."browse/part_svc.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/quick-charge.cgi b/httemplate/edit/process/quick-charge.cgi
new file mode 100644 (file)
index 0000000..477f585
--- /dev/null
@@ -0,0 +1,32 @@
+<%
+
+#untaint custnum
+$cgi->param('custnum') =~ /^(\d+)$/
+  or die 'illegal custnum '. $cgi->param('custnum');
+my $custnum = $1;
+
+$cgi->param('amount') =~ /^\s*(\d+(\.\d{1,2})?)\s*$/
+  or die 'illegal amount '. $cgi->param('amount');
+my $amount = $1;
+
+my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+  or die "unknown custnum $custnum";
+
+my $error = $cust_main->charge(
+  $amount,
+  $cgi->param('pkg'),
+  '$'. sprintf("%.2f",$amount),
+  $cgi->param('taxclass')
+);
+
+if ($error) {
+%>
+<!-- mason kludge -->
+<%
+  eidiot($error);
+} else {
+  print $cgi->redirect(popurl(3). "view/cust_main.cgi?$custnum" );
+}
+
+%>
+
diff --git a/httemplate/edit/process/quick-cust_pkg.cgi b/httemplate/edit/process/quick-cust_pkg.cgi
new file mode 100644 (file)
index 0000000..a8f5b14
--- /dev/null
@@ -0,0 +1,24 @@
+<%
+
+#untaint custnum
+$cgi->param('custnum') =~ /^(\d+)$/
+  or die 'illegal custnum '. $cgi->param('custnum');
+my $custnum = $1;
+$cgi->param('pkgpart') =~ /^(\d+)$/
+  or die 'illegal pkgpart '. $cgi->param('pkgpart');
+my $pkgpart = $1;
+
+my @cust_pkg = ();
+my $error = FS::cust_pkg::order($custnum, [ $pkgpart ], [], \@cust_pkg, );
+
+if ($error) {
+%>
+<!-- mason kludge -->
+<%
+  eidiot($error);
+} else {
+  print $cgi->redirect(popurl(3). "view/cust_pkg.cgi?". $cust_pkg[0]->pkgnum );
+}
+
+%>
+
diff --git a/httemplate/edit/process/router.cgi b/httemplate/edit/process/router.cgi
new file mode 100644 (file)
index 0000000..1b7fc38
--- /dev/null
@@ -0,0 +1,101 @@
+<%
+
+use FS::UID qw(dbh);
+
+my $dbh = dbh;
+local $FS::UID::AutoCommit=0;
+
+sub check {
+  my $error = shift;
+  if($error) {
+    $cgi->param('error', $error);
+    print $cgi->redirect(popurl(3) . "edit/router.cgi?". $cgi->query_string);
+    $dbh->rollback;
+    exit;
+  }
+}
+
+my $error = '';
+my $routernum  = $cgi->param('routernum');
+my $routername = $cgi->param('routername');
+my $old = qsearchs('router', { routernum => $routernum });
+my @old_rf;
+my @old_psr;
+
+my $new = new FS::router {
+    routernum  => $routernum,
+    routername => $routername,
+    svcnum     => 0
+    };
+
+if($old) {
+  if($old->routername ne $new->routername) {
+    $error = $new->replace($old);
+  } #else do nothing
+} else {
+  $error = $new->insert;
+  $routernum = $new->routernum;
+}
+
+check($error);
+
+if ($old) {
+  @old_psr = $old->part_svc_router;
+  foreach $psr (@old_psr) {
+    if($cgi->param('svcpart_'.$psr->svcpart) eq 'ON') {
+      # do nothing
+    } else {
+      $error = $psr->delete;
+    }
+  }
+  check($error);
+  @old_rf = $old->router_field;
+  foreach $rf (@old_rf) {
+    if(my $new_val = $cgi->param('rf_'.$rf->routerfieldpart)) {
+      if($new_val ne $rf->value) {
+        my $new_rf = new FS::router_field 
+         { routernum       => $routernum,
+           value           => $new_val,
+           routerfieldpart => $rf->routerfieldpart };
+       $error = $new_rf->replace($rf);
+      } #else do nothing
+    } else {
+      $error = $rf->delete;
+    }
+    check($error);
+  }
+}
+
+foreach($cgi->param) {
+  if($cgi->param($_) eq 'ON' and /^svcpart_(\d+)$/) {
+    my $svcpart = $1;
+    if(grep {$_->svcpart == $svcpart} @old_psr) {
+      # do nothing
+    } else {
+      my $new_psr = new FS::part_svc_router { svcpart   => $svcpart,
+                                              routernum => $routernum };
+      $error = $new_psr->insert;
+    }
+    check($error);
+  } elsif($cgi->param($_) ne '' and /^rf_(\d+)$/) {
+    my $part = $1;
+    if(my @x = grep {$_->routerfieldpart == $part} @old_rf) {
+      # already handled all of these
+    } else {
+      my $new_rf = new FS::router_field
+        { routernum       => $routernum,
+         value           => $cgi->param('rf_'.$part),
+         routerfieldpart => $part };
+      $error = $new_rf->insert;
+      check($error);
+    }
+  }
+}
+
+
+
+# Yay, everything worked!
+$dbh->commit or die $dbh->errstr;
+print $cgi->redirect(popurl(3). "browse/router.cgi");
+
+%>
diff --git a/httemplate/edit/process/svc_acct.cgi b/httemplate/edit/process/svc_acct.cgi
new file mode 100755 (executable)
index 0000000..950a860
--- /dev/null
@@ -0,0 +1,49 @@
+<%
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum = $1;
+
+my $old;
+if ( $svcnum ) {
+  $old = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
+    or die "fatal: can't find account (svcnum $svcnum)!";
+} else {
+  $old = '';
+}
+
+#unmunge popnum
+$cgi->param('popnum', (split(/:/, $cgi->param('popnum') ))[0] );
+
+#unmunge passwd
+if ( $cgi->param('_password') eq '*HIDDEN*' ) {
+  die "fatal: no previous account to recall hidden password from!" unless $old;
+  $cgi->param('_password',$old->getfield('_password'));
+}
+
+#unmunge usergroup
+$cgi->param('usergroup', [ $cgi->param('radius_usergroup') ] );
+
+my $new = new FS::svc_acct ( {
+  map {
+    $_, scalar($cgi->param($_));
+  #} qw(svcnum pkgnum svcpart username _password popnum uid gid finger dir
+  #  shell quota slipip)
+  } ( fields('svc_acct'), qw( pkgnum svcpart usergroup ) )
+} );
+
+my $error;
+if ( $svcnum ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $svcnum = $new->svcnum;
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "svc_acct.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "view/svc_acct.cgi?" . $svcnum );
+}
+
+%>
diff --git a/httemplate/edit/process/svc_acct_pop.cgi b/httemplate/edit/process/svc_acct_pop.cgi
new file mode 100755 (executable)
index 0000000..46ad74d
--- /dev/null
@@ -0,0 +1,28 @@
+<%
+
+my $popnum = $cgi->param('popnum');
+
+my $old = qsearchs('svc_acct_pop',{'popnum'=>$popnum}) if $popnum;
+
+my $new = new FS::svc_acct_pop ( {
+  map {
+    $_, scalar($cgi->param($_));
+  } fields('svc_acct_pop')
+} );
+
+my $error = '';
+if ( $popnum ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $popnum=$new->getfield('popnum');
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "svc_acct_pop.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "browse/svc_acct_pop.cgi");
+}
+
+%>
diff --git a/httemplate/edit/process/svc_broadband.cgi b/httemplate/edit/process/svc_broadband.cgi
new file mode 100644 (file)
index 0000000..ab8b9f9
--- /dev/null
@@ -0,0 +1,79 @@
+<%
+
+# If it's stupid but it works, it's not stupid.
+# -- U.S. Army
+
+local $FS::UID::AutoCommit = 0;
+my $dbh = FS::UID::dbh;
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum = $1;
+
+my $old; my @old_sbf;
+if ( $svcnum ) {
+  $old = qsearchs('svc_broadband', { 'svcnum' => $svcnum } )
+    or die "fatal: can't find broadband service (svcnum $svcnum)!";
+  @old_sbf = $old->sb_field;
+} else {
+  $old = '';
+}
+
+my $new = new FS::svc_broadband ( {
+  map {
+    ($_, scalar($cgi->param($_)));
+  } ( fields('svc_broadband'), qw( pkgnum svcpart ) )
+} );
+
+my $error;
+if ( $svcnum ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $svcnum = $new->svcnum;
+}
+
+unless ($error) {
+  my $sb_field;
+
+  foreach ($cgi->param) {
+    #warn "\$cgi->param $_: " . $cgi->param($_);
+    if(/^sbf_(\d+)/) {
+      my $part = $1;
+      #warn "\$part $part";
+      $sb_field = new FS::sb_field 
+        { svcnum      => $svcnum,
+          value       => $cgi->param($_),
+          sbfieldpart => $part };
+      if (my @x = grep { $_->sbfieldpart eq $part } @old_sbf) {
+      #if (my $old_sb_field = (grep { $_->sbfieldpart eq $part} @old_Sbf)[0]) {
+        #warn "array: " . scalar(@x);
+        if (length($sb_field->value) && ($sb_field->value ne $x[0]->value)) { 
+          #warn "replacing " . $x[0]->value . " with " . $sb_field->value;
+          $error = $sb_field->replace($x[0]);
+          #$error = $sb_field->replace($old_sb_field);
+        } elsif (length($sb_field->value) == 0) { 
+          #warn "delete";
+          $error = $x[0]->delete;
+        }
+      } else {
+        if (length($sb_field->value) > 0) { 
+          #warn "insert";
+          $error = $sb_field->insert;
+        }
+        # else do nothing
+      }
+    }
+  }
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  $cgi->param('ip_addr', $new->ip_addr);
+  $dbh->rollback;
+  print $cgi->redirect(popurl(2). "svc_broadband.cgi?". $cgi->query_string );
+} else {
+  $dbh->commit or die $dbh->errstr;
+  print $cgi->redirect(popurl(3). "view/svc_broadband.cgi?" . $svcnum );
+}
+
+%>
diff --git a/httemplate/edit/process/svc_domain.cgi b/httemplate/edit/process/svc_domain.cgi
new file mode 100755 (executable)
index 0000000..19f8eb4
--- /dev/null
@@ -0,0 +1,31 @@
+<%
+
+#remove this to actually test the domains!
+$FS::svc_domain::whois_hack = 1;
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum = $1;
+
+my $new = new FS::svc_domain ( {
+  map {
+    $_, scalar($cgi->param($_));
+  #} qw(svcnum pkgnum svcpart domain action purpose)
+  } ( fields('svc_domain'), qw( pkgnum svcpart action purpose ) )
+} );
+
+my $error = '';
+if ($cgi->param('svcnum')) {
+  $error="Can't modify a domain!";
+} else {
+  $error=$new->insert;
+  $svcnum=$new->svcnum;
+}
+
+if ($error) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "svc_domain.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "view/svc_domain.cgi?$svcnum");
+}
+
+%>
diff --git a/httemplate/edit/process/svc_forward.cgi b/httemplate/edit/process/svc_forward.cgi
new file mode 100755 (executable)
index 0000000..bb066d8
--- /dev/null
@@ -0,0 +1,29 @@
+<%
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum =$1;
+
+my $old = qsearchs('svc_forward',{'svcnum'=>$svcnum}) if $svcnum;
+
+my $new = new FS::svc_forward ( {
+  map {
+    ($_, scalar($cgi->param($_)));
+  } ( fields('svc_forward'), qw( pkgnum svcpart ) )
+} );
+
+my $error = '';
+if ( $svcnum ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $svcnum = $new->getfield('svcnum');
+} 
+
+if ($error) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "svc_forward.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "view/svc_forward.cgi?$svcnum");
+}
+
+%>
diff --git a/httemplate/edit/process/svc_www.cgi b/httemplate/edit/process/svc_www.cgi
new file mode 100644 (file)
index 0000000..4091314
--- /dev/null
@@ -0,0 +1,36 @@
+<%
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum = $1;
+
+my $old;
+if ( $svcnum ) {
+  $old = qsearchs('svc_www', { 'svcnum' => $svcnum } )
+    or die "fatal: can't find website (svcnum $svcnum)!";
+} else {
+  $old = '';
+}
+
+my $new = new FS::svc_www ( {
+  map {
+    ($_, scalar($cgi->param($_)));
+  #} qw(svcnum pkgnum svcpart recnum usersvc)
+  } ( fields('svc_www'), qw( pkgnum svcpart ) )
+} );
+
+my $error;
+if ( $svcnum ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $svcnum = $new->svcnum;
+}
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "svc_www.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "view/svc_www.cgi?" . $svcnum );
+}
+
+%>
diff --git a/httemplate/edit/router.cgi b/httemplate/edit/router.cgi
new file mode 100755 (executable)
index 0000000..b524c64
--- /dev/null
@@ -0,0 +1,92 @@
+<HTML><BODY>
+
+<%
+
+my $router;
+if ( $cgi->keywords ) {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $router = qsearchs('router', { routernum => $1 }) 
+      or print $cgi->redirect(popurl(2)."browse/router.cgi") ;
+} else {
+  $router = new FS::router ( {
+    map { $_, scalar($cgi->param($_)) } fields('router')
+  } );
+}
+
+my $routernum = $router->routernum;
+my $action = $routernum ? 'Edit' : 'Add';
+my $hashref = $router->hashref;
+
+print header("$action Router", menubar(
+  'Main Menu' => "$p",
+  'View all routers' => "${p}browse/router.cgi",
+));
+
+if($cgi->param('error')) {
+%> <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+<% } %>
+
+<FORM ACTION="<%=popurl(1)%>process/router.cgi" METHOD=POST>
+  <INPUT TYPE="hidden" NAME="routernum" VALUE="<%=$routernum%>">
+    Router #<%=$routernum or "(NEW)"%>
+
+<BR><BR>Name <INPUT TYPE="text" NAME="routername" SIZE=32 VALUE="<%=$hashref->{routername}%>">
+
+<BR><BR>
+Custom fields:
+<BR>
+<%=table() %>
+
+<%
+# I know, I know.  Massive false laziness with edit/svc_broadband.cgi.  But 
+# Kristian won't let me generalize the custom field mechanism to every table in 
+# the database, so this is what we get.  <snarl>
+# -- MW
+
+my @part_router_field = qsearch('part_router_field', { });
+my %rf = map { $_->part_router_field->name, $_->value } $router->router_field;
+foreach (sort { $a->name cmp $b->name } @part_router_field) {
+  %>
+  <TR>
+    <TD ALIGN="right"><%=$_->name%></TD>
+    <TD><%
+  if(my @opts = $_->list_values) {
+    %>  <SELECT NAME="rf_<%=$_->routerfieldpart%>" SIZE="1">
+          <%
+    foreach $opt (@opts) {
+      %>  <OPTION VALUE="<%=$opt%>"<%=($opt eq $rf{$_->name}) 
+              ? ' SELECTED' : ''%>>
+            <%=$opt%>
+         </OPTION>
+   <% } %>
+       </SELECT>
+ <% } else { %>
+        <INPUT NAME="rf_<%=$_->routerfieldpart%>"
+        VALUE="<%=$rf{$_->name}%>"
+        <%=$_->length ? 'SIZE="'.$_->length.'"' : ''%>>
+  <% } %></TD>
+  </TR>
+<% } %>
+</TABLE>
+
+
+
+<BR><BR>Select the service types available on this router<BR>
+<%
+
+foreach my $part_svc ( qsearch('part_svc', { svcdb    => 'svc_broadband',
+                                             disabled => '' }) ) {
+  %>
+  <BR>
+  <INPUT TYPE="checkbox" NAME="svcpart_<%=$part_svc->svcpart%>"<%=
+      qsearchs('part_svc_router', { svcpart   => $part_svc->svcpart, 
+                                    routernum => $routernum } ) ? 'CHECKED' : ''%> VALUE="ON">
+  <A HREF="<%=${p}%>edit/part_svc.cgi?<%=$part_svc->svcpart%>">
+    <%=$part_svc->svcpart%>: <%=$part_svc->svc%></A>
+  <% } %>
+
+  <BR><BR><INPUT TYPE="submit" VALUE="Apply changes">
+  </FORM>
+</BODY></HTML>
+
diff --git a/httemplate/edit/svc_acct.cgi b/httemplate/edit/svc_acct.cgi
new file mode 100755 (executable)
index 0000000..4420bb6
--- /dev/null
@@ -0,0 +1,293 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+my @shells = $conf->config('shells');
+
+my($svcnum, $pkgnum, $svcpart, $part_svc, $svc_acct, @groups);
+if ( $cgi->param('error') ) {
+  $svc_acct = new FS::svc_acct ( {
+    map { $_, scalar($cgi->param($_)) } fields('svc_acct')
+  } );
+  $svcnum = $svc_acct->svcnum;
+  $pkgnum = $cgi->param('pkgnum');
+  $svcpart = $cgi->param('svcpart');
+  $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
+  die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
+  @groups = $cgi->param('radius_usergroup');
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $svcnum=$1;
+    $svc_acct=qsearchs('svc_acct',{'svcnum'=>$svcnum})
+      or die "Unknown (svc_acct) svcnum!";
+
+    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+      or die "Unknown (cust_svc) svcnum!";
+
+    $pkgnum=$cust_svc->pkgnum;
+    $svcpart=$cust_svc->svcpart;
+
+    $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
+    die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
+
+    @groups = $svc_acct->radius_groups;
+
+  } else { #adding
+
+    $svc_acct = new FS::svc_acct({}); 
+
+    foreach $_ (split(/-/,$query)) {
+      $pkgnum=$1 if /^pkgnum(\d+)$/;
+      $svcpart=$1 if /^svcpart(\d+)$/;
+    }
+    $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
+    die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
+
+    $svcnum='';
+
+    #set gecos
+    my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+    if ($cust_pkg) {
+      my($cust_main)=qsearchs('cust_main',{'custnum'=> $cust_pkg->custnum } );
+      unless ( $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
+        $svc_acct->setfield('finger',
+          $cust_main->getfield('first') . " " . $cust_main->getfield('last')
+        );
+      }
+    }
+
+    #set fixed and default fields from part_svc
+    foreach my $part_svc_column (
+      grep { $_->columnflag } $part_svc->all_part_svc_column
+    ) {
+      if ( $part_svc_column->columnname eq 'usergroup' ) {
+        @groups = split(',', $part_svc_column->columnvalue);
+      } else {
+        $svc_acct->setfield( $part_svc_column->columnname,
+                             $part_svc_column->columnvalue,
+                           );
+      }
+    }
+
+  }
+}
+
+#fixed radius groups always override & display
+if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
+  @groups = split(',', $part_svc->part_svc_column('usergroup')->columnvalue);
+}
+
+my $action = $svcnum ? 'Edit' : 'Add';
+
+my $svc = $part_svc->getfield('svc');
+
+my $otaker = getotaker;
+
+my $username = $svc_acct->username;
+my $password;
+if ( $svc_acct->_password ) {
+  if ( $conf->exists('showpasswords') || ! $svcnum ) {
+    $password = $svc_acct->_password;
+  } else {
+    $password = "*HIDDEN*";
+  }
+} else {
+  $password = '';
+}
+
+my $ulen = $conf->config('usernamemax')
+           || $svc_acct->dbdef_table->column('username')->length;
+my $ulen2 = $ulen+2;
+
+my $pmax = $conf->config('passwordmax') || 8;
+my $pmax2 = $pmax+2;
+
+my $p1 = popurl(1);
+print header("$action $svc account");
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT><BR><BR>"
+  if $cgi->param('error');
+
+print 'Service # '. ( $svcnum ? "<B>$svcnum</B>" : " (NEW)" ). '<BR>'.
+      'Service: <B>'. $part_svc->svc. '</B><BR><BR>'.
+      <<END;
+    <FORM NAME="OneTrueForm" ACTION="${p1}process/svc_acct.cgi" METHOD=POST>
+      <INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">
+      <INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
+      <INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">
+END
+
+print &ntable("#cccccc",2), <<END;
+<TR><TD ALIGN="right">Username</TD>
+<TD><INPUT TYPE="text" NAME="username" VALUE="$username" SIZE=$ulen2 MAXLENGTH=$ulen></TD></TR>
+<TR><TD ALIGN="right">Password</TD>
+<TD><INPUT TYPE="text" NAME="_password" VALUE="$password" SIZE=$pmax2 MAXLENGTH=$pmax>
+(blank to generate)</TD>
+</TR>
+END
+
+my $sec_phrase = $svc_acct->sec_phrase;
+if ( $conf->exists('security_phrase') ) {
+  print <<END;
+  <TR><TD ALIGN="right">Security phrase</TD>
+  <TD><INPUT TYPE="text" NAME="sec_phrase" VALUE="$sec_phrase" SIZE=32>
+    (for forgotten passwords)</TD>
+  </TD>
+END
+} else {
+  print qq!<INPUT TYPE="hidden" NAME="sec_phrase" VALUE="$sec_phrase">!;
+}
+
+#domain
+my $domsvc = $svc_acct->domsvc || 0;
+if ( $part_svc->part_svc_column('domsvc')->columnflag eq 'F' ) {
+  print qq!<INPUT TYPE="hidden" NAME="domsvc" VALUE="$domsvc">!;
+} else { 
+  my %svc_domain = ();
+
+  if ( $domsvc ) {
+    my $svc_domain = qsearchs('svc_domain', { 'svcnum' => $domsvc, } );
+    if ( $svc_domain ) {
+      $svc_domain{$svc_domain->svcnum} = $svc_domain;
+    } else {
+      warn "unknown svc_domain.svcnum for svc_acct.domsvc: $domsvc";
+    }
+  }
+
+  if ( $part_svc->part_svc_column('domsvc')->columnflag eq 'D' ) {
+    my $svc_domain = qsearchs('svc_domain', {
+      'svcnum' => $part_svc->part_svc_column('domsvc')->columnvalue,
+    } );
+    if ( $svc_domain ) {
+      $svc_domain{$svc_domain->svcnum} = $svc_domain;
+    } else {
+      warn "unknown svc_domain.svcnum for part_svc_column domsvc: ".
+           $part_svc->part_svc_column('domsvc')->columnvalue;
+    }
+  }
+
+  my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $pkgnum } );
+  if ($cust_pkg && !$conf->exists('svc_acct-alldomains') ) {
+    my @cust_svc =
+      map { qsearch('cust_svc', { 'pkgnum' => $_->pkgnum } ) }
+          qsearch('cust_pkg', { 'custnum' => $cust_pkg->custnum } );
+    foreach my $cust_svc ( @cust_svc ) {
+      my $svc_domain =
+        qsearchs('svc_domain', { 'svcnum' => $cust_svc->svcnum } );
+     $svc_domain{$svc_domain->svcnum} = $svc_domain if $svc_domain;
+    }
+  } else {
+    %svc_domain = map { $_->svcnum => $_ } qsearch('svc_domain', {} );
+  }
+  print qq!<TR><TD ALIGN="right">Domain</TD>!.
+        qq!<TD><SELECT NAME="domsvc" SIZE=1>\n!;
+  foreach my $svcnum (
+    sort { $svc_domain{$a}->domain cmp $svc_domain{$b}->domain }
+      keys %svc_domain
+  ) {
+    my $svc_domain = $svc_domain{$svcnum};
+    print qq!<OPTION VALUE="!. $svc_domain->svcnum. qq!"!.
+          ( $svc_domain->svcnum == $domsvc ? ' SELECTED' : '' ).
+          '>'. $svc_domain->domain. "\n" ;
+  }
+  print "</SELECT></TD></TR>";
+}
+
+#pop
+my $popnum = $svc_acct->popnum || 0;
+if ( $part_svc->part_svc_column('popnum')->columnflag eq "F" ) {
+  print qq!<INPUT TYPE="hidden" NAME="popnum" VALUE="$popnum">!;
+} else { 
+  print qq!<TR><TD ALIGN="right">Access number</TD>!.
+        qq!<TD>!. FS::svc_acct_pop::popselector($popnum). '</TD></TR>';
+}
+
+my($uid,$gid,$finger,$dir)=(
+  $svc_acct->uid,
+  $svc_acct->gid,
+  $svc_acct->finger,
+  $svc_acct->dir,
+);
+
+print <<END;
+<INPUT TYPE="hidden" NAME="uid" VALUE="$uid">
+<INPUT TYPE="hidden" NAME="gid" VALUE="$gid">
+END
+
+if ( !$finger && $part_svc->part_svc_column('uid')->columnflag eq 'F' ) {
+  print '<INPUT TYPE="hidden" NAME="finger" VALUE="">';
+} else {
+  print '<TR><TD ALIGN="right">GECOS</TD>'.
+        qq!<TD><INPUT TYPE="text" NAME="finger" VALUE="$finger"></TD></TR>!;
+}
+print qq!<INPUT TYPE="hidden" NAME="dir" VALUE="$dir">!;
+
+my $shell = $svc_acct->shell;
+if ( $part_svc->part_svc_column('shell')->columnflag eq "F"
+     || ( !$shell && $part_svc->part_svc_column('uid')->columnflag eq 'F' )
+   ) {
+  print qq!<INPUT TYPE="hidden" NAME="shell" VALUE="$shell">!;
+} else {
+  print qq!<TR><TD ALIGN="right">Shell</TD><TD><SELECT NAME="shell" SIZE=1>!;
+  my($etc_shell);
+  foreach $etc_shell (@shells) {
+    print "<OPTION", $etc_shell eq $shell ? ' SELECTED' : '', ">",
+          $etc_shell, "\n";
+  }
+  print "</SELECT></TD></TR>";
+}
+
+my($quota,$slipip)=(
+  $svc_acct->quota,
+  $svc_acct->slipip,
+);
+
+if ( $part_svc->part_svc_column('quota')->columnflag eq "F" )
+{
+  print qq!<INPUT TYPE="hidden" NAME="quota" VALUE="$quota">!;
+} else {
+  print <<END;
+    <TR><TD ALIGN="right">Quota:</TD>
+        <TD> <INPUT TYPE="text" NAME="quota" VALUE="$quota" ></TD>
+    </TR>
+END
+}
+
+if ( $part_svc->part_svc_column('slipip')->columnflag eq "F" ) {
+  print qq!<INPUT TYPE="hidden" NAME="slipip" VALUE="$slipip">!;
+} else {
+  print qq!<TR><TD ALIGN="right">IP</TD><TD><INPUT TYPE="text" NAME="slipip" VALUE="$slipip"></TD></TR>!;
+}
+
+foreach my $r ( grep { /^r(adius|[cr])_/ } fields('svc_acct') ) {
+  $r =~ /^^r(adius|[cr])_(.+)$/ or next; #?
+  my $a = $2;
+  if ( $part_svc->part_svc_column($r)->columnflag eq 'F' ) {
+    print qq!<INPUT TYPE="hidden" NAME="$r" VALUE="!.
+          $svc_acct->getfield($r). '">';
+  } else {
+    print qq!<TR><TD ALIGN="right">$FS::raddb::attrib{$a}</TD><TD><INPUT TYPE="text" NAME="$r" VALUE="!.
+          $svc_acct->getfield($r). '"></TD></TR>';
+  }
+}
+
+print '<TR><TD ALIGN="right">RADIUS groups</TD>';
+if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
+  print '<TD BGCOLOR="#ffffff">'. join('<BR>', @groups);
+} else {
+  print '<TD>'. &FS::svc_acct::radius_usergroup_selector( \@groups );
+}
+print '</TD></TR>';
+
+#submit
+print qq!</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">!; 
+
+print <<END;
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/svc_acct_pop.cgi b/httemplate/edit/svc_acct_pop.cgi
new file mode 100755 (executable)
index 0000000..399502a
--- /dev/null
@@ -0,0 +1,56 @@
+<!-- mason kludge -->
+<%
+
+my $svc_acct_pop;
+if ( $cgi->param('error') ) {
+  $svc_acct_pop = new FS::svc_acct_pop ( {
+    map { $_, scalar($cgi->param($_)) } fields('svc_acct_pop')
+  } );
+} elsif ( $cgi->keywords ) { #editing
+  my($query)=$cgi->keywords;
+  $query =~ /^(\d+)$/;
+  $svc_acct_pop=qsearchs('svc_acct_pop',{'popnum'=>$1});
+} else { #adding
+  $svc_acct_pop = new FS::svc_acct_pop {};
+}
+my $action = $svc_acct_pop->popnum ? 'Edit' : 'Add';
+my $hashref = $svc_acct_pop->hashref;
+
+my $p1 = popurl(1);
+print header("$action Access Number", menubar(
+  'Main Menu' => popurl(2),
+  'View all Access Numbers' => popurl(2). "browse/svc_acct_pop.cgi",
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/svc_acct_pop.cgi" METHOD=POST>!;
+
+#display
+
+print qq!<INPUT TYPE="hidden" NAME="popnum" VALUE="$hashref->{popnum}">!,
+      "POP #", $hashref->{popnum} ? $hashref->{popnum} : "(NEW)";
+
+print <<END;
+<PRE>
+City      <INPUT TYPE="text" NAME="city" SIZE=32 VALUE="$hashref->{city}">
+State     <INPUT TYPE="text" NAME="state" SIZE=16 MAXLENGTH=16 VALUE="$hashref->{state}">
+Area Code <INPUT TYPE="text" NAME="ac" SIZE=4 MAXLENGTH=3 VALUE="$hashref->{ac}">
+Exchange  <INPUT TYPE="text" NAME="exch" SIZE=4 MAXLENGTH=3 VALUE="$hashref->{exch}">
+Local     <INPUT TYPE="text" NAME="loc" SIZE=5 MAXLENGTH=4 VALUE="$hashref->{loc}">
+</PRE>
+END
+
+print qq!<BR><INPUT TYPE="submit" VALUE="!,
+      $hashref->{popnum} ? "Apply changes" : "Add Access Number",
+      qq!">!;
+
+print <<END;
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/svc_broadband.cgi b/httemplate/edit/svc_broadband.cgi
new file mode 100644 (file)
index 0000000..ee7f8be
--- /dev/null
@@ -0,0 +1,192 @@
+<!-- mason kludge -->
+<%
+
+# If it's stupid but it works, it's still stupid.
+#  -Kristian
+
+
+use HTML::Widgets::SelectLayers;
+use Tie::IxHash;
+
+my( $svcnum,  $pkgnum, $svcpart, $part_svc, $svc_broadband );
+if ( $cgi->param('error') ) {
+  $svc_broadband = new FS::svc_broadband ( {
+    map { $_, scalar($cgi->param($_)) } fields('svc_broadband')
+  } );
+  $svcnum = $svc_broadband->svcnum;
+  $pkgnum = $cgi->param('pkgnum');
+  $svcpart = $cgi->param('svcpart');
+  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+  die "No part_svc entry!" unless $part_svc;
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $svcnum=$1;
+    $svc_broadband=qsearchs('svc_broadband',{'svcnum'=>$svcnum})
+      or die "Unknown (svc_broadband) svcnum!";
+
+    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+      or die "Unknown (cust_svc) svcnum!";
+
+    $pkgnum=$cust_svc->pkgnum;
+    $svcpart=$cust_svc->svcpart;
+  
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+  } else { #adding
+
+    $svc_broadband = new FS::svc_broadband({});
+
+    foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+      $pkgnum=$1 if /^pkgnum(\d+)$/;
+      $svcpart=$1 if /^svcpart(\d+)$/;
+    }
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+    $svc_broadband->setfield('svcpart', $svcpart);
+
+    $svcnum='';
+
+    #set fixed and default fields from part_svc
+    foreach my $part_svc_column (
+      grep { $_->columnflag } $part_svc->all_part_svc_column
+    ) {
+      $svc_broadband->setfield( $part_svc_column->columnname,
+                                $part_svc_column->columnvalue,
+                              );
+    }
+
+  }
+}
+my $action = $svc_broadband->svcnum ? 'Edit' : 'Add';
+
+if ($pkgnum) {
+
+  #Nothing?
+
+} elsif ( $action eq 'Edit' ) {
+
+  #Nothing?
+
+} else {
+  die "\$action eq Add, but \$pkgnum is null!\n";
+}
+
+my $p1 = popurl(1);
+
+my ($ip_addr, $speed_up, $speed_down, $blocknum) =
+    ($svc_broadband->ip_addr,
+     $svc_broadband->speed_up,
+     $svc_broadband->speed_down,
+     $svc_broadband->blocknum);
+
+%>
+
+<%=header("Broadband Service $action", '')%>
+
+<% if ($cgi->param('error')) { %>
+<FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT><BR>
+<% } %>
+
+Service #<B><%=$svcnum ? $svcnum : "(NEW)"%></B><BR><BR>
+
+<FORM ACTION="<%=${p1}%>process/svc_broadband.cgi" METHOD=POST>
+  <INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+  <INPUT TYPE="hidden" NAME="pkgnum" VALUE="<%=$pkgnum%>">
+  <INPUT TYPE="hidden" NAME="svcpart" VALUE="<%=$svcpart%>">
+
+  <%=&ntable("#cccccc",2)%>
+    <TR>
+      <TD ALIGN="right">IP Address</TD>
+      <TD BGCOLOR="#ffffff">
+<% if ( $part_svc->part_svc_column('ip_addr')->columnflag eq 'F' ) { %>
+        <INPUT TYPE="hidden" NAME="ip_addr" VALUE="<%=$ip_addr%>"><%=$ip_addr%>
+<% } else { %>
+        <INPUT TYPE="text" NAME="ip_addr" VALUE="<%=$ip_addr%>">
+<% } %>
+      </TD>
+    </TR>
+    <TR>
+      <TD ALIGN="right">Download speed</TD>
+      <TD BGCOLOR="#ffffff">
+<% if ( $part_svc->part_svc_column('speed_down')->columnflag eq 'F' ) { %>
+        <INPUT TYPE="hidden" NAME="speed_down" VALUE="<%=$speed_down%>"><%=$speed_down%>Kbps
+<% } else { %>
+    <INPUT TYPE="text" NAME="speed_down" SIZE=5 VALUE="<%=$speed_down%>">Kbps
+<% } %>
+      </TD>
+    </TR>
+    <TR>
+      <TD ALIGN="right">Upload speed</TD>
+      <TD BGCOLOR="#ffffff">
+<% if ( $part_svc->part_svc_column('speed_up')->columnflag eq 'F' ) { %>
+        <INPUT TYPE="hidden" NAME="speed_up" VALUE="<%=$speed_up%>"><%=$speed_up%>Kbps
+<% } else { %>
+        <INPUT TYPE="text" NAME="speed_up" SIZE=5 VALUE="<%=$speed_up%>">Kbps
+<% } %>
+      </TD>
+    </TR>
+<% if ($action eq 'Add') { %>
+    <TR>
+      <TD ALIGN="right">Router/Block</TD>
+      <TD BGCOLOR="#ffffff">
+        <SELECT NAME="blocknum">
+<%
+  foreach my $router ($svc_broadband->allowed_routers) {
+    foreach my $addr_block ($router->addr_block) {
+%>
+        <OPTION VALUE="<%=$addr_block->blocknum%>"<%=($addr_block->blocknum eq $blocknum) ? ' SELECTED' : ''%>>
+          <%=$router->routername%>:<%=$addr_block->ip_gateway%>/<%=$addr_block->ip_netmask%></OPTION>
+<%
+    }
+  }
+%>
+        </SELECT>
+      </TD>
+    </TR>
+<% } else { %>
+
+    <TR>
+      <TD ALIGN="right">Router/Block</TD>
+      <TD BGCOLOR="#ffffff">
+        <%=$svc_broadband->addr_block->router->routername%>:<%=$svc_broadband->addr_block->NetAddr%>
+        <INPUT TYPE="hidden" NAME="blocknum" VALUE="<%=$svc_broadband->blocknum%>">
+      </TD>
+    </TR>
+
+<% } %>
+
+<%
+
+  my @part_sb_field = qsearch('part_sb_field', { svcpart => $svcpart });
+  my $sbf_hashref = $svc_broadband->sb_field_hashref($svcpart);
+  foreach (sort { $a->name cmp $b->name } @part_sb_field) {
+    %>
+    <TR>
+      <TD ALIGN="right"><%=$_->name%></TD>
+      <TD><%
+      if(my @opts = $_->list_values) {
+        %>
+       <SELECT NAME="sbf_<%=$_->sbfieldpart%>" SIZE=1> <%
+        foreach $opt (@opts) { %>
+          <OPTION VALUE="<%=$opt%>"<%=
+           ($opt eq $sbf_hashref->{$_->name}) ? ' SELECTED' : ''%>>
+           <%=$opt%></OPTION><%
+        } %></SELECT>
+   <% } else { %>
+        <INPUT NAME="sbf_<%=$_->sbfieldpart%>"
+           VALUE="<%=$sbf_hashref->{$_->name}%>"
+     <%=$_->length ? 'SIZE="'.$_->length.'"' : ''%>>
+   <% } %>
+      </TD>
+    </TR>
+<% } %>
+  </TABLE>
+  <BR>
+  <INPUT TYPE="submit" NAME="submit" VALUE="Submit">
+</FORM>
+</BODY>
+</HTML>
+
diff --git a/httemplate/edit/svc_domain.cgi b/httemplate/edit/svc_domain.cgi
new file mode 100755 (executable)
index 0000000..ca0e339
--- /dev/null
@@ -0,0 +1,98 @@
+<!-- mason kludge -->
+<%
+
+my($svcnum, $pkgnum, $svcpart, $kludge_action, $purpose, $part_svc,
+   $svc_domain);
+if ( $cgi->param('error') ) {
+  $svc_domain = new FS::svc_domain ( {
+    map { $_, scalar($cgi->param($_)) } fields('svc_domain')
+  } );
+  $svcnum = $svc_domain->svcnum;
+  $pkgnum = $cgi->param('pkgnum');
+  $svcpart = $cgi->param('svcpart');
+  $kludge_action = $cgi->param('action');
+  $purpose = $cgi->param('purpose');
+  $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
+  die "No part_svc entry!" unless $part_svc;
+} else {
+  $kludge_action = '';
+  $purpose = '';
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $svcnum=$1;
+    $svc_domain=qsearchs('svc_domain',{'svcnum'=>$svcnum})
+      or die "Unknown (svc_domain) svcnum!";
+
+    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+      or die "Unknown (cust_svc) svcnum!";
+
+    $pkgnum=$cust_svc->pkgnum;
+    $svcpart=$cust_svc->svcpart;
+
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+  } else { #adding
+
+    $svc_domain = new FS::svc_domain({});
+  
+    foreach $_ (split(/-/,$query)) {
+      $pkgnum=$1 if /^pkgnum(\d+)$/;
+      $svcpart=$1 if /^svcpart(\d+)$/;
+    }
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+    $svcnum='';
+
+    #set fixed and default fields from part_svc
+    foreach my $part_svc_column (
+      grep { $_->columnflag } $part_svc->all_part_svc_column
+    ) {
+      $svc_domain->setfield( $part_svc_column->columnname,
+                             $part_svc_column->columnvalue,
+                           );
+    }
+
+  }
+
+}
+my $action = $svcnum ? 'Edit' : 'Add';
+
+my $svc = $part_svc->getfield('svc');
+
+my $otaker = getotaker;
+
+my $domain = $svc_domain->domain;
+
+my $p1 = popurl(1);
+print header("$action $svc", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print <<END;
+    <FORM ACTION="${p1}process/svc_domain.cgi" METHOD=POST>
+      <INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">
+      <INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
+      <INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">
+END
+
+print qq!<INPUT TYPE="radio" NAME="action" VALUE="N"!;
+print ' CHECKED' if $kludge_action eq 'N';
+print qq!>New!;
+print qq!<BR><INPUT TYPE="radio" NAME="action" VALUE="M"!;
+print ' CHECKED' if $kludge_action eq 'M';
+print qq!>Transfer!;
+
+print <<END;
+<P>Domain <INPUT TYPE="text" NAME="domain" VALUE="$domain" SIZE=28 MAXLENGTH=63>
+<BR>Purpose/Description: <INPUT TYPE="text" NAME="purpose" VALUE="$purpose" SIZE=64>
+<P><INPUT TYPE="submit" VALUE="Submit">
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/edit/svc_forward.cgi b/httemplate/edit/svc_forward.cgi
new file mode 100755 (executable)
index 0000000..0d815b9
--- /dev/null
@@ -0,0 +1,175 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my($svcnum, $pkgnum, $svcpart, $part_svc, $svc_forward);
+if ( $cgi->param('error') ) {
+  $svc_forward = new FS::svc_forward ( {
+    map { $_, scalar($cgi->param($_)) } fields('svc_forward')
+  } );
+  $svcnum = $svc_forward->svcnum;
+  $pkgnum = $cgi->param('pkgnum');
+  $svcpart = $cgi->param('svcpart');
+  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+  die "No part_svc entry!" unless $part_svc;
+} else {
+
+  my($query) = $cgi->keywords;
+
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $svcnum=$1;
+    $svc_forward=qsearchs('svc_forward',{'svcnum'=>$svcnum})
+      or die "Unknown (svc_forward) svcnum!";
+
+    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+      or die "Unknown (cust_svc) svcnum!";
+
+    $pkgnum=$cust_svc->pkgnum;
+    $svcpart=$cust_svc->svcpart;
+  
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+  } else { #adding
+
+    $svc_forward = new FS::svc_forward({});
+
+    foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+      $pkgnum=$1 if /^pkgnum(\d+)$/;
+      $svcpart=$1 if /^svcpart(\d+)$/;
+    }
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+    $svcnum='';
+
+    #set fixed and default fields from part_svc
+    foreach my $part_svc_column (
+      grep { $_->columnflag } $part_svc->all_part_svc_column
+    ) {
+      $svc_forward->setfield( $part_svc_column->columnname,
+                              $part_svc_column->columnvalue,
+                            );
+    }
+  }
+
+}
+my $action = $svc_forward->svcnum ? 'Edit' : 'Add';
+
+my %email;
+if ($pkgnum) {
+
+  #find all possible user svcnums (and emails)
+
+  #starting with those currently attached
+  if ( $svc_forward->srcsvc ) {
+    my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_forward->srcsvc } );
+    $email{$svc_forward->srcsvc} = $svc_acct->email;
+  }
+  if ( $svc_forward->dstsvc ) {
+    my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $svc_forward->dstsvc } );
+    $email{$svc_forward->dstsvc} = $svc_acct->email;
+  }
+
+  #and including the rest for this customer
+  my($u_part_svc,@u_acct_svcparts);
+  foreach $u_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_acct'}) ) {
+    push @u_acct_svcparts,$u_part_svc->getfield('svcpart');
+  }
+
+  my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+  my($custnum)=$cust_pkg->getfield('custnum');
+  my($i_cust_pkg);
+  foreach $i_cust_pkg ( qsearch('cust_pkg',{'custnum'=>$custnum}) ) {
+    my($cust_pkgnum)=$i_cust_pkg->getfield('pkgnum');
+    my($acct_svcpart);
+    foreach $acct_svcpart (@u_acct_svcparts) {   #now find the corresponding 
+                                              #record(s) in cust_svc ( for this
+                                              #pkgnum ! )
+      foreach my $i_cust_svc (
+        qsearch( 'cust_svc', { 'pkgnum'  => $cust_pkgnum,
+                               'svcpart' => $acct_svcpart } )
+      ) {
+        my $svc_acct =
+          qsearchs( 'svc_acct', { 'svcnum' => $i_cust_svc->svcnum } );
+        $email{$svc_acct->svcnum} = $svc_acct->email;
+      }  
+    }
+  }
+
+} elsif ( $action eq 'Edit' ) {
+
+  my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$svc_forward->srcsvc});
+  $email{$svc_forward->srcsvc} = $svc_acct->email;
+
+  $svc_acct=qsearchs('svc_acct',{'svcnum'=>$svc_forward->dstsvc});
+  $email{$svc_forward->dstsvc} = $svc_acct->email;
+
+} else {
+  die "\$action eq Add, but \$pkgnum is null!\n";
+}
+
+my($srcsvc,$dstsvc,$dst)=(
+  $svc_forward->srcsvc,
+  $svc_forward->dstsvc,
+  $svc_forward->dst,
+);
+
+#display
+
+%>
+
+<%= header("Mail Forward $action") %>
+
+<% if ( $cgi->param('error') ) { %>
+  <FONT SIZE="+1" COLOR="#ff0000">Error: <%= $cgi->param('error') %></FONT>
+  <BR><BR>
+<% } %>
+
+Service #<%= $svcnum ? "<B>$svcnum</B>" : " (NEW)" %><BR>
+Service: <B><%= $part_svc->svc %></B><BR><BR>
+
+<FORM NAME="dummy">
+
+<%= ntable("#cccccc",2) %>
+<TR><TD ALIGN="right">Email to</TD><TD><SELECT NAME="srcsvc" SIZE=1>
+<% foreach $_ (keys %email) { %>
+  <OPTION<%= $_ eq $srcsvc ? " SELECTED" : "" %> VALUE="<%= $_ %>"><%= $email{$_} %></OPTION>
+<% } %>
+</SELECT></TD></TR>
+
+<%
+  tie my %tied_email, 'Tie::IxHash',
+    ''  => 'SELECT DESTINATION',
+    %email,
+    '0' => '(other email address)';
+  my $widget = new HTML::Widgets::SelectLayers(
+    'selected_layer' => $dstsvc,
+    'options'        => \%tied_email,
+    'form_name'      => 'dummy',
+    'form_action'    => 'process/svc_forward.cgi',
+    'form_select'    => ['srcsvc'],
+    'html_between'   => '</TD></TR></TABLE>',
+    'layer_callback' => sub {
+      my $layer = shift;
+      my $html = qq!<INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">!.
+                 qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!.
+                 qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!.
+                 qq!<INPUT TYPE="hidden" NAME="dstsvc" VALUE="$layer">!;
+      if ( $layer eq '0' ) {
+        $html .= ntable("#cccccc",2).
+                 '<TR><TD ALIGN="right">Destination email</TD>'.
+                 qq!<TD><INPUT TYPE="text" NAME="dst" VALUE="$dst"></TD>!.
+                 '</TR></TABLE>';
+      }
+      $html .= '<BR><INPUT TYPE="submit" VALUE="Submit">';
+      $html;
+    },
+  );
+%>
+
+<TR><TD ALIGN="right">Forwards to</TD>
+<TD><%= $widget->html %>
+  </BODY>
+</HTML>
diff --git a/httemplate/edit/svc_www.cgi b/httemplate/edit/svc_www.cgi
new file mode 100644 (file)
index 0000000..d2c9ade
--- /dev/null
@@ -0,0 +1,178 @@
+<!-- mason kludge -->
+<%
+
+my( $svcnum,  $pkgnum, $svcpart, $part_svc, $svc_www );
+if ( $cgi->param('error') ) {
+  $svc_www = new FS::svc_www ( {
+    map { $_, scalar($cgi->param($_)) } fields('svc_www')
+  } );
+  $svcnum = $svc_www->svcnum;
+  $pkgnum = $cgi->param('pkgnum');
+  $svcpart = $cgi->param('svcpart');
+  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+  die "No part_svc entry!" unless $part_svc;
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $svcnum=$1;
+    $svc_www=qsearchs('svc_www',{'svcnum'=>$svcnum})
+      or die "Unknown (svc_www) svcnum!";
+
+    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+      or die "Unknown (cust_svc) svcnum!";
+
+    $pkgnum=$cust_svc->pkgnum;
+    $svcpart=$cust_svc->svcpart;
+  
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+  } else { #adding
+
+    $svc_www = new FS::svc_www({});
+
+    foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+      $pkgnum=$1 if /^pkgnum(\d+)$/;
+      $svcpart=$1 if /^svcpart(\d+)$/;
+    }
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+    $svcnum='';
+
+    #set fixed and default fields from part_svc
+    foreach my $part_svc_column (
+      grep { $_->columnflag } $part_svc->all_part_svc_column
+    ) {
+      $svc_www->setfield( $part_svc_column->columnname,
+                          $part_svc_column->columnvalue,
+                        );
+    }
+
+  }
+}
+my $action = $svc_www->svcnum ? 'Edit' : 'Add';
+
+my( %username, %arec );
+if ($pkgnum) {
+
+  my($u_part_svc,@u_acct_svcparts);
+  foreach $u_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_acct'}) ) {
+    push @u_acct_svcparts,$u_part_svc->getfield('svcpart');
+  }
+
+  my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+  my($custnum)=$cust_pkg->getfield('custnum');
+  my($i_cust_pkg);
+  foreach $i_cust_pkg ( qsearch('cust_pkg',{'custnum'=>$custnum}) ) {
+    my($cust_pkgnum)=$i_cust_pkg->getfield('pkgnum');
+    my($acct_svcpart);
+    foreach $acct_svcpart (@u_acct_svcparts) {   #now find the corresponding 
+                                              #record(s) in cust_svc ( for this
+                                              #pkgnum ! )
+      my($i_cust_svc);
+      foreach $i_cust_svc ( qsearch('cust_svc',{'pkgnum'=>$cust_pkgnum,'svcpart'=>$acct_svcpart}) ) {
+        my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$i_cust_svc->getfield('svcnum')});
+        $username{$svc_acct->getfield('svcnum')}=$svc_acct->getfield('username');
+      }  
+    }
+  }
+
+
+  my($d_part_svc,@d_acct_svcparts);
+  foreach $d_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_domain'}) ) {
+    push @d_acct_svcparts,$d_part_svc->getfield('svcpart');
+  }
+
+  foreach $i_cust_pkg ( qsearch('cust_pkg',{'custnum'=>$custnum}) ) {
+    my($cust_pkgnum)=$i_cust_pkg->getfield('pkgnum');
+    my($acct_svcpart);
+    foreach $acct_svcpart (@d_acct_svcparts) {
+      my($i_cust_svc);
+      foreach $i_cust_svc ( qsearch('cust_svc',{'pkgnum'=>$cust_pkgnum,'svcpart'=>$acct_svcpart}) ) {
+        my($svc_domain)=qsearchs('svc_domain',{'svcnum'=>$i_cust_svc->getfield('svcnum')});
+        my $domain_rec;
+        foreach $domain_rec ( qsearch('domain_record',{
+            'svcnum'  => $svc_domain->svcnum,
+            'rectype' => 'A' } ),
+        qsearch('domain_record',{
+            'svcnum'  => $svc_domain->svcnum,
+            'rectype' => 'CNAME'
+            } ) ) {
+          $arec{$domain_rec->recnum} =
+            $domain_rec->reczone eq '@'
+              ? $svc_domain->domain
+              : $domain_rec->reczone. '.'. $svc_domain->domain;
+        }
+        $arec{'@.'. $svc_domain->domain} = $svc_domain->domain
+          unless qsearchs('domain_record', { svcnum  => $svc_domain->svcnum,
+                                             reczone => '@',                } );
+        $arec{'www.'. $svc_domain->domain} = 'www.'. $svc_domain->domain
+          unless qsearchs('domain_record', { svcnum  => $svc_domain->svcnum,
+                                             reczone => 'www',              } );
+      }
+    }
+  }
+
+} elsif ( $action eq 'Edit' ) {
+
+  my($domain_rec) = qsearchs('domain_record', { 'recnum'=>$svc_www->recnum });
+  $arec{$svc_www->recnum} = join '.', $domain_rec->recdata, $domain_rec->reczone;
+
+} else {
+  die "\$action eq Add, but \$pkgnum is null!\n";
+}
+
+
+my $p1 = popurl(1);
+print header("Web Hosting $action", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/svc_www.cgi" METHOD=POST>!;
+
+#display
+
+
+#svcnum
+print qq!<INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">!;
+print qq!Service #<B>!, $svcnum ? $svcnum : "(NEW)", "</B><BR><BR>";
+
+#pkgnum
+print qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!;
+#svcpart
+print qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
+
+my($recnum,$usersvc)=(
+  $svc_www->recnum,
+  $svc_www->usersvc,
+);
+
+print &ntable("#cccccc",2),
+      '<TR><TD ALIGN="right">Zone</TD><TD><SELECT NAME="recnum" SIZE=1>';
+foreach $_ (keys %arec) {
+  print "<OPTION", $_ eq $recnum ? " SELECTED" : "",
+        qq! VALUE="$_">$arec{$_}!;
+}
+print "</SELECT></TD></TR>";
+
+print '<TR><TD ALIGN="right">Username</TD><TD><SELECT NAME="usersvc" SIZE=1>';
+foreach $_ (keys %username) {
+  print "<OPTION", ($_ eq $usersvc) ? " SELECTED" : "",
+        qq! VALUE="$_">$username{$_}!;
+}
+print "</SELECT></TD></TR>";
+
+print '</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">';
+
+print <<END;
+
+    </FORM>
+  </BODY>
+</HTML>
+END
+%>
diff --git a/httemplate/graph/money_time-graph.cgi b/httemplate/graph/money_time-graph.cgi
new file mode 100755 (executable)
index 0000000..944019a
--- /dev/null
@@ -0,0 +1,108 @@
+<%
+
+#my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
+my ($curmon,$curyear) = (localtime(time))[4,5];
+
+#find first month
+my $syear = $cgi->param('syear') || 1899+$curyear;
+my $smonth = $cgi->param('smonth') || $curmon+1;
+
+#find last month
+my $eyear = $cgi->param('eyear') || 1900+$curyear;
+my $emonth = $cgi->param('emonth') || $curmon+1;
+if ( $emonth++>12 ) { $emonth-=12; $eyear++; }
+
+my @labels;
+my %data;
+
+while ( $syear < $eyear || ( $syear == $eyear && $smonth < $emonth ) ) {
+  push @labels, "$smonth/$syear";
+
+  my $speriod = timelocal(0,0,0,1,$smonth-1,$syear);
+  if ( ++$smonth == 13 ) { $syear++; $smonth=1; }
+  my $eperiod = timelocal(0,0,0,1,$smonth-1,$syear);
+
+  my $where = "WHERE _date >= $speriod AND _date < $eperiod";
+
+  # Invoiced
+  my $charged_sql = "SELECT SUM(charged) FROM cust_bill $where";
+  my $charged_sth = dbh->prepare($charged_sql) or die dbh->errstr;
+  $charged_sth->execute or die $charged_sth->errstr;
+  my $charged = $charged_sth->fetchrow_arrayref->[0] || 0;
+
+  push @{$data{charged}}, $charged;
+
+  #accounts receivable
+#  my $ar_sql2 = "SELECT SUM(amount) FROM cust_credit $where";
+  my $credited_sql = "SELECT SUM(cust_credit_bill.amount) FROM cust_credit_bill, cust_bill WHERE cust_bill.invnum = cust_credit_bill.invnum AND cust_bill._date >= $speriod AND cust_bill._date < $eperiod";
+  my $credited_sth = dbh->prepare($credited_sql) or die dbh->errstr;
+  $credited_sth->execute or die $credited_sth->errstr;
+  my $credited = $credited_sth->fetchrow_arrayref->[0] || 0;
+
+    #horrible local kludge
+    my $expenses_sql = "SELECT SUM(cust_bill_pkg.setup) FROM cust_bill_pkg, cust_bill, cust_pkg, part_pkg WHERE cust_bill.invnum = cust_bill_pkg.invnum AND cust_bill._date >= $speriod AND cust_bill._date < $eperiod AND cust_pkg.pkgnum = cust_bill_pkg.pkgnum AND cust_pkg.pkgpart = part_pkg.pkgpart AND LOWER(part_pkg.pkg) LIKE 'expense _%'";
+    my $expenses_sth = dbh->prepare($expenses_sql) or die dbh->errstr;
+    $expenses_sth->execute or die $expenses_sth->errstr;
+    my $expenses = $expenses_sth->fetchrow_arrayref->[0] || 0;
+
+  push @{$data{ar}}, $charged-$credited-$expenses;
+
+  #deferred revenue
+#  push @{$data{defer}}, '0';
+
+  #cashflow
+  my $paid_sql = "SELECT SUM(paid) FROM cust_pay $where";
+  my $paid_sth = dbh->prepare($paid_sql) or die dbh->errstr;
+  $paid_sth->execute or die $paid_sth->errstr;
+  my $paid = $paid_sth->fetchrow_arrayref->[0] || 0;
+
+  my $refunded_sql = "SELECT SUM(refund) FROM cust_refund $where";
+  my $refunded_sth = dbh->prepare($refunded_sql) or die dbh->errstr;
+  $refunded_sth->execute or die $refunded_sth->errstr;
+  my $refunded = $refunded_sth->fetchrow_arrayref->[0] || 0;
+
+    #horrible local kludge that doesn't even really work right
+    my $expenses_sql = "SELECT SUM(cust_bill_pay.amount) FROM cust_bill_pay, cust_bill WHERE cust_bill_pay.invnum = cust_bill.invnum AND cust_bill_pay._date >= $speriod AND cust_bill_pay._date < $eperiod AND 0 < ( select count(*) from cust_bill_pkg, cust_pkg, part_pkg WHERE cust_bill.invnum = cust_bill_pkg.invnum AND cust_pkg.pkgnum = cust_bill_pkg.pkgnum AND cust_pkg.pkgpart = part_pkg.pkgpart AND LOWER(part_pkg.pkg) LIKE 'expense _%' )";
+
+#    my $expenses_sql = "SELECT SUM(cust_bill_pay.amount) FROM cust_bill_pay, cust_bill_pkg, cust_bill, cust_pkg, part_pkg WHERE cust_bill_pay.invnum = cust_bill.invnum AND cust_bill.invnum = cust_bill_pkg.invnum AND cust_bill_pay._date >= $speriod AND cust_bill_pay._date < $eperiod AND cust_pkg.pkgnum = cust_bill_pkg.pkgnum AND cust_pkg.pkgpart = part_pkg.pkgpart AND LOWER(part_pkg.pkg) LIKE 'expense _%'";
+    my $expenses_sth = dbh->prepare($expenses_sql) or die dbh->errstr;
+    $expenses_sth->execute or die $expenses_sth->errstr;
+    my $expenses = $expenses_sth->fetchrow_arrayref->[0] || 0;
+
+  push @{$data{cash}}, $paid-$refunded-$expenses;
+
+}
+
+#my $chart = Chart::LinesPoints->new(1024,480);
+my $chart = Chart::LinesPoints->new(768,480);
+
+$chart->set(
+  #'min_val' => 0,
+  'legend' => 'bottom',
+  'legend_labels' => [ #'Invoiced (cust_bill)',
+                       'Accounts receivable (invoices - applied credits)',
+                       #'Deferred revenue',
+                       'Actual cashflow (payments - refunds)' ],
+);
+
+my @data = ( \@labels,
+             #map $data{$_}, qw( ar defer cash )
+             #map $data{$_}, qw( charged ar cash )
+             map $data{$_}, qw( ar cash )
+           );
+
+#my $gd = $chart->plot(\@data);
+#open (IMG, ">i_r_c.png");
+#print IMG $gd->png;
+#close IMG;
+
+#$chart->png("i_r_c.png", \@data);
+
+#$chart->cgi_png(\@data);
+
+http_header('Content-Type' => 'image/png' );
+$Response->{ContentType} = 'image/png';
+
+$chart->_set_colors();
+
+%><%= $chart->scalar_png(\@data) %>
diff --git a/httemplate/graph/money_time.cgi b/httemplate/graph/money_time.cgi
new file mode 100644 (file)
index 0000000..e24157c
--- /dev/null
@@ -0,0 +1,59 @@
+<!-- mason kludge %>
+<%
+
+#my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
+my ($curmon,$curyear) = (localtime(time))[4,5];
+
+#find first month
+my $syear = $cgi->param('syear') || 1899+$curyear;
+my $smonth = $cgi->param('smonth') || $curmon+1;
+
+#find last month
+my $eyear = $cgi->param('eyear') || 1900+$curyear;
+my $emonth = $cgi->param('emonth') || $curmon+1;
+
+%>
+
+<HTML>
+  <HEAD>
+    <TITLE>Graphing monetary values over time</TITLE>
+  </HEAD>
+<BODY BGCOLOR="#e8e8e8">
+<IMG SRC="money_time-graph.cgi?<%= $cgi->query_string %>" WIDTH="768" HEIGHT="480">
+<BR>
+<FORM METHOD="POST">
+<INPUT TYPE="checkbox" NAME="ar">
+  Accounts receivable (invoices - applied credits)<BR>
+<INPUT TYPE="checkbox" NAME="charged">
+  Just Invoices<BR>
+<INPUT TYPE="checkbox" NAME="defer">
+  Accounts receivable, with deferred revenue (invoices - applied credits, with charges for annual/semi-annual/quarterly/etc. services deferred over applicable time period) (there has got to be a shorter description for this)<BR>
+<INPUT TYPE="checkbox" NAME="cash">
+  Cashflow (payments - refunds)<BR>
+<BR>
+From <SELECT NAME="smonth">
+<% my @m = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
+   foreach my $m ( 1..12 ) { %>
+<OPTION VALUE="<%= $m %>"<%= $m == $smonth ? ' SELECTED' : '' %>><%= $m[$m-1] %>
+<% } %>
+</SELECT>
+<SELECT NAME="syear">
+<% foreach my $y ( 1999 .. 2010 ) { %>
+<OPTION VALUE="<%= $y %>"<%= $y == $syear ? ' SELECTED' : '' %>><%= $y %>
+<% } %>
+</SELECT>
+ to <SELECT NAME="emonth">
+<% foreach my $m ( 1..12 ) { %>
+<OPTION VALUE="<%= $m %>"<%= $m == $emonth ? ' SELECTED' : '' %>><%= $m[$m-1] %>
+<% } %>
+</SELECT>
+<SELECT NAME="eyear">
+<% foreach my $y ( 1999 .. 2010 ) { %>
+<OPTION VALUE="<%= $y %>"<%= $y == $eyear ? ' SELECTED' : '' %>><%= $y %>
+<% } %>
+</SELECT>
+
+<INPUT TYPE="submit" VALUE="Graph">
+</FORM>
+</BODY>
+</HTML>
diff --git a/httemplate/images/mid-logo.png b/httemplate/images/mid-logo.png
new file mode 100644 (file)
index 0000000..d993419
Binary files /dev/null and b/httemplate/images/mid-logo.png differ
diff --git a/httemplate/images/small-logo.png b/httemplate/images/small-logo.png
new file mode 100644 (file)
index 0000000..406a369
Binary files /dev/null and b/httemplate/images/small-logo.png differ
diff --git a/httemplate/index.html b/httemplate/index.html
new file mode 100644 (file)
index 0000000..017ffcd
--- /dev/null
@@ -0,0 +1,223 @@
+<HTML>
+  <HEAD>
+    <TITLE>
+      Freeside Main Menu
+    </TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#FFFFFF">
+  <table width="100%">
+    <tr><td>
+        <IMG BORDER=0 ALT="Silicon Interactive Software Design" SRC="images/small-logo.png">
+    </td><td>
+      <font color="#ff0000" size=7>freeside main menu</font>
+    </td><td align=right valign=bottom>
+      version %%%VERSION%%%
+      <BR><A HREF="http://www.sisd.com/freeside">Freeside home page</A>
+      <BR><A HREF="docs/">Documentation</A>
+    </td></tr>
+  </table>
+
+<BR>
+[<A NAME="customer_service" style="background-color: #cccccc"> Sales / Customer service </A>]
+[ <A HREF="#bookkeeping">Bookkeeping / Collections</A> ]
+[ <A HREF="#reports">Reports</A> ]
+[ <A HREF="#sysadmin">Sysadmin</A> ]
+    <TABLE CELLSPACING=2 CELLPADDING=0 BORDER=0" WIDTH="100%" BGCOLOR="#eeeeee">
+    <TR><TH BGCOLOR="#cccccc">Sales / Customer service</TH></TR>
+    <TR><TD>
+        <BR><FONT SIZE="+1"><A HREF="edit/cust_main.cgi">New Customer</A></FONT>
+        <BR>
+        <BR><FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="custnum_on" VALUE="1">Customer # <INPUT TYPE="text" NAME="custnum_text"><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/cust_main.cgi?browse=custnum">all customers by customer number</A></FORM>
+        <FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="last_on" VALUE="1">Last name <INPUT TYPE="text" NAME="last_text"><SELECT NAME="last_type"><OPTION SELECTED VALUE="All">(all)</OPTION><OPTION>Fuzzy<OPTION>Substring</OPTION><OPTION>Exact</OPTION></SELECT><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/cust_main.cgi?browse=last">all customers by last name</A></FORM>
+        <FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="company_on" VALUE="1">Company <INPUT TYPE="text" NAME="company_text"><SELECT NAME="company_type"><OPTION SELECTED VALUE="All">(all)</OPTION><OPTION>Fuzzy<OPTION>Substring</OPTION><OPTION>Exact</OPTION></SELECT><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/cust_main.cgi?browse=company">all customers by company</A></FORM>
+<!--        <FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="address2_on" VALUE="1">Unit <INPUT TYPE="text" NAME="address2_text"><INPUT TYPE="submit" VALUE="Search"></FORM>-->
+        <FORM ACTION="search/cust_main.cgi" METHOD="POST"><INPUT TYPE="hidden" NAME="phone_on" VALUE="1">Phone # <INPUT TYPE="text" NAME="phone_text"><INPUT TYPE="submit" VALUE="Search"></FORM>
+        <BR><FORM ACTION="search/svc_acct.cgi" METHOD="POST">Username <INPUT TYPE="text" NAME="username"><SELECT NAME="username_type"><OPTION VALUE="All">(all)</OPTION><OPTION>Fuzzy</OPTION><OPTION>Substring</OPTION><OPTION SELECTED>Exact</OPTION></SELECT><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/svc_acct.cgi?username">all accounts by username</A> or <A HREF="search/svc_acct.cgi?uid">uid</A></FORM>
+        <BR><FORM ACTION="search/svc_domain.cgi" METHOD="POST">Domain <INPUT TYPE="text" NAME="domain"><INPUT TYPE="submit" VALUE="Search"> or <A HREF="search/svc_domain.cgi?domain">all domains</A></FORM>
+<!--        <LI><A HREF="search/svc_forward.html">mail forwards (by ?)</A>-->
+      <BR>
+    </TD></TR>
+    </TABLE>
+
+
+
+    <BR><BR><BR>
+
+
+[ <A HREF="#customer_service">Sales / Customer service</A> ]
+[<A NAME="bookkeeping" style="background-color: #cccccc"> Bookkeeping / Collections </A>]
+[ <A HREF="#reports">Reports</A> ]
+[ <A HREF="#sysadmin">Sysadmin</A> ]
+    <TABLE CELLSPACING=2 CELLPADDING=0 BORDER=0 WIDTH="100%" BGCOLOR="#eeeeee">
+    <TR><TH BGCOLOR="#cccccc">Bookkeeping / Collections</TH></TR>
+    <TR><TD>
+      <BR><A HREF="search/cust_main-quickpay.html">Quick payment entry</A>
+      <BR>
+      <BR><FORM ACTION="search/cust_main.cgi" METHOD="POST">Credit card # <INPUT TYPE="hidden" NAME="card_on" VALUE="1"><INPUT TYPE="text" NAME="card"><INPUT TYPE="submit" VALUE="Search"></FORM>
+      <FORM ACTION="search/cust_bill.cgi" METHOD="POST">Invoice # <INPUT TYPE="text" NAME="invnum" SIZE="8"><INPUT TYPE="submit" VALUE="Search"></FORM>
+      <FORM ACTION="search/cust_pay.cgi" METHOD="POST">Check # <INPUT TYPE="text" NAME="payinfo" SIZE="8"><INPUT TYPE="hidden" NAME="payby" VALUE="BILL"><INPUT TYPE="submit" VALUE="Search"></FORM>
+      <BR><A HREF="browse/cust_pay_batch.cgi">View pending credit card batch</A>      <BR><BR><A HREF="search/cust_pkg.html">Packages (by next bill date range)</A>
+      <BR><BR>Invoice reports
+            <UL>
+              <LI><a href="search/cust_bill_event.html">Invoice event errors (failed credit cards)</a>
+              <LI>open invoices (<A HREF="search/cust_bill.cgi?OPEN_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN_custnum">by customer number</A>)
+              <LI>30 day open invoices (<A HREF="search/cust_bill.cgi?OPEN30_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN30_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN30_custnum">by customer number</A>)
+              <LI>60 day open invoices (<A HREF="search/cust_bill.cgi?OPEN60_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN60_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN60_custnum">by customer number</A>)
+              <LI>90 day open invoices (<A HREF="search/cust_bill.cgi?OPEN90_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN90_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN90_custnum">by customer number</A>)
+              <LI>120 day open invoices (<A HREF="search/cust_bill.cgi?OPEN120_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN120_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN120_custnum">by customer number</A>)
+              <LI>all invoices (<A HREF="search/cust_bill.cgi?invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?date">by date</A>) (<A HREF="search/cust_bill.cgi?custnum">by customer number</A>)
+            </UL>
+      <A HREF="search/report_cust_pay.html">Payment report (by type and/or date range)</A>
+      <BR><BR>Financial reports
+            <UL>
+              <LI> <A HREF="search/report_receivables.cgi">current receivables</A>
+              <LI> <A HREF="search/report_tax.html">tax reports</A>
+              <LI> <A HREF="search/report_cc.html">credit card receipts</A>
+              <LI> <A HREF="search/report_credit.html">credit memos</A>
+            </UL>
+      <CENTER><HR WIDTH="94%" NOSHADE></CENTER><BR>
+      <A NAME="admin">Administration</a>
+        <ul>
+          <LI><A HREF="browse/part_pkg.cgi">View/Edit package definitions</A>
+            - One or more services are grouped together into a package and
+              given pricing information.  Customers purchase packages, not
+              services.
+<!--          <LI><A HREF="browse/agent_type.cgi">View/Edit agent types</A>
+            - Agent types define groups of package definitions that you can
+              then assign to particular agents.
+          <LI><A HREF="browse/agent.cgi">View/Edit agents</A>
+            - Agents are resellers of your service.  Agents may be limited
+              to a subset of your full offerings (via their type).
+-->
+          <LI><A HREF="browse/cust_main_county.cgi">View/Edit locales and tax rates</A>
+            - Change tax rates, or break down a country into states, or a state
+              into counties and assign different tax rates to each.
+          <LI><A HREF="browse/part_bill_event.cgi">View/Edit invoice events</A> - Actions for overdue invoices
+        </ul>
+      <BR>
+    </TD></TR>
+    </TABLE>
+
+
+
+    <BR><BR><BR>
+
+
+
+[ <A HREF="#customer_service">Sales / Customer service</A> ]
+[ <A HREF="#bookkeeping">Bookkeeping / Collections</A> ]
+[<A NAME="reports" style="background-color: #cccccc"> Reports </A>]
+[ <A HREF="#sysadmin">Sysadmin</A> ]
+    <TABLE CELLSPACING=2 CELLPADDING=0 BORDER=0 WIDTH="100%" BGCOLOR="#eeeeee">
+    <TR><TH BGCOLOR="#cccccc">Reports</TH></TR>
+    <TR><TD>
+      <BR>
+      Auditing pre-Freeside services with no customer record
+      <UL>
+        <LI>unlinked accounts (<A HREF="search/svc_acct.cgi?UN_svcnum">by service number</A>) (<A HREF="search/svc_acct.cgi?UN_username">by username</A>) (<A HREF="search/svc_acct.cgi?UN_uid">by uid</A>)
+<!--        <LI>unlinked mail forwards (<A HREF="search/svc_forward.cgi?UN_svcnum">by service number</A>) (by ?)) -->
+        <LI>unlinked domains (<A HREF="search/svc_domain.cgi?UN_svcnum">by service number</A>) (<A HREF="search/svc_domain.cgi?UN_domain">by domain</A>)
+      </UL>
+      Packages
+      <UL>
+        <LI><A HREF="search/cust_pkg.cgi?pkgnum">all packages (by package number)</A>
+        <LI><A HREF="search/cust_pkg.cgi?SUSP_pkgnum">suspended packages (by package number)</A>
+        <LI><A HREF="search/cust_pkg.cgi?APKG_pkgnum">packages with unconfigured services (by package number)</A>
+        <LI><A HREF="search/cust_pkg.html">packages (by next bill date range)</A>
+      </UL>
+      <A HREF="browse/part_pkg.cgi?active=1">Package definitions (by number of active packages)</A>
+      <BR><BR>Invoices
+      <UL>
+        <LI><a href="search/cust_bill_event.html">Invoice event errors (failed credit cards)</a>
+        <LI>open invoices (<A HREF="search/cust_bill.cgi?OPEN_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN_custnum">by customer number</A>)
+        <LI>30 day open invoices (<A HREF="search/cust_bill.cgi?OPEN30_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN30_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN30_custnum">by customer number</A>)
+        <LI>60 day open invoices (<A HREF="search/cust_bill.cgi?OPEN60_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN60_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN60_custnum">by customer number</A>)
+        <LI>90 day open invoices (<A HREF="search/cust_bill.cgi?OPEN90_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN90_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN90_custnum">by customer number</A>)
+        <LI>120 day open invoices (<A HREF="search/cust_bill.cgi?OPEN120_invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?OPEN120_date">by date</A>) (<A HREF="search/cust_bill.cgi?OPEN120_custnum">by customer number</A>)
+        <LI>all invoices (<A HREF="search/cust_bill.cgi?invnum">by invoice number</A>) (<A HREF="search/cust_bill.cgi?date">by date</A>) (<A HREF="search/cust_bill.cgi?custnum">by customer number</A>)
+      </UL>
+    <A HREF="search/report_cust_pay.html">Payment Report (by type and/or date range)</A>
+    <BR><BR>Financial reports
+            <UL>
+              <LI> <A HREF="search/report_receivables.cgi">current receivables</A>
+              <LI> <A HREF="search/report_tax.html">tax reports</A>
+              <LI> <A HREF="search/report_cc.html">credit card receipts</A>
+              <LI> <A HREF="search/report_credit.html">credit memos</A>
+            </UL>
+    Customers
+      <UL>
+        <LI><A HREF="search/cust_main-otaker.cgi">Search customers by order-taker</A>
+      </UL>
+    <FORM ACTION="search/sql.cgi" METHOD="POST">SQL query: <TT>SELECT </TT><INPUT TYPE="text" NAME="sql" SIZE=32><INPUT TYPE="submit" VALUE="Query"></FORM>
+
+    <BR>
+    </TD></TR>
+    </TABLE>
+
+
+
+    <BR><BR><BR>
+
+
+[ <A HREF="#customer_service">Sales / Customer service</A> ]
+[ <A HREF="#bookkeeping">Bookkeeping / Collections</A> ]
+[ <A HREF="#reports">Reports</A> ]
+[<A NAME="sysadmin" style="background-color: #cccccc"> Sysadmin </A>]
+    <TABLE CELLSPACING=2 CELLPADDING=0 BORDER=0 WIDTH="100%" BGCOLOR="#eeeeee">
+    <TR><TH BGCOLOR="#cccccc">Sysadmin</TH></TR>
+    <TR><TD>
+      <BR>
+      <A HREF="browse/nas.cgi">View active NAS ports</A>
+      <BR><A HREF="browse/queue.cgi">View pending job queue</A>
+      <BR><A HREF="misc/cust_main-import.cgi">Batch import customers from CSV file</A>
+      <BR><A HREF="misc/cust_main-import_charges.cgi">Batch import charges from CSV file</A>
+      <BR><BR><CENTER><HR WIDTH="94%" NOSHADE></CENTER><BR>
+      <A NAME="config" HREF="config/config-view.cgi">Configuration</a><!-- - <font size="+2" color="#ff0000">start here</font> -->
+      <BR><BR><A NAME="admin">Administration</a>
+        <ul>
+          <LI><A HREF="browse/part_export.cgi">View/Edit exports</A>
+            - Provisioning services to external machines, databases and APIs.
+          <LI><A HREF="browse/part_svc.cgi">View/Edit service definitions</A>
+            - Services are items you offer to your customers.
+          <LI><A HREF="browse/part_pkg.cgi">View/Edit package definitions</A>
+            - One or more services are grouped together into a package and
+              given pricing information.  Customers purchase packages, not
+              services.
+          <LI><A HREF="browse/agent_type.cgi">View/Edit agent types</A>
+            - Agent types define groups of package definitions that you can
+              then assign to particular agents.
+          <LI><A HREF="browse/agent.cgi">View/Edit agents</A>
+            - Agents are resellers of your service.  Agents may be limited
+              to a subset of your full offerings (via their type).
+          <LI><A HREF="browse/part_referral.cgi">View/Edit advertising sources</A>
+            - Where a customer heard about your service.  Tracked for
+              informational purposes.
+          <LI><A HREF="browse/cust_main_county.cgi">View/Edit locales and tax rates</A>
+            - Change tax rates, or break down a country into states, or a state
+              into counties and assign different tax rates to each.
+          <LI><A HREF="browse/svc_acct_pop.cgi">View/Edit Access Numbers</A>
+            - Points of Presence 
+          <LI><A HREF="browse/part_bill_event.cgi">View/Edit invoice events</A> - Actions for overdue invoices
+         <LI><A HREF="browse/msgcat.cgi">View/Edit message catalog</A> - Change error messages and other customizable labels.
+         <LI><A HREF="browse/part_sb_field.cgi">View/Edit custom svc_broadband fields</A>
+         - Custom broadband service fields for site-specific export/informational data.
+         <LI><A HREF="browse/generic.cgi?part_router_field">View/Edit custom router fields</A>
+         - Custom router fields for site-specific export data.
+         <LI><A HREF="browse/router.cgi">View/Edit routers</A>
+         - Broadband access routers
+         <LI><A HREF="browse/addr_block.cgi">View/Edit address blocks</A>
+         - Manage address blocks and block assignments to broadband routers.
+        </ul>
+        <BR>
+      </TD></TR>
+      </TABLE>
+      <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+      <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+      <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+      <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+      <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+      <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+      <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+      <BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+  </BODY>
+</HTML>
diff --git a/httemplate/misc/bill.cgi b/httemplate/misc/bill.cgi
new file mode 100755 (executable)
index 0000000..44d85b8
--- /dev/null
@@ -0,0 +1,38 @@
+<%
+
+#untaint custnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d*)$/;
+my $custnum = $1;
+my $cust_main = qsearchs('cust_main',{'custnum'=>$custnum});
+die "Can't find customer!\n" unless $cust_main;
+
+my $error = $cust_main->bill(
+#                          'time'=>$time
+                         );
+#&eidiot($error) if $error;
+
+unless ( $error ) {
+  $cust_main->apply_payments;
+  $cust_main->apply_credits;
+
+  $error = $cust_main->collect(
+  #                             'invoice-time'=>$time,
+                               #'batch_card'=> 'yes',
+                               #'batch_card'=> 'no',
+                               #'report_badcard'=> 'yes',
+                               #'retry_card' => 'yes',
+                               'retry' => 'yes',
+                              );
+}
+#&eidiot($error) if $error;
+
+if ( $error ) {
+%>
+<!-- mason kludge -->
+<%
+  &idiot($error);
+} else {
+  print $cgi->redirect(popurl(2). "view/cust_main.cgi?$custnum");
+}
+%>
diff --git a/httemplate/misc/cancel-unaudited.cgi b/httemplate/misc/cancel-unaudited.cgi
new file mode 100755 (executable)
index 0000000..11cde96
--- /dev/null
@@ -0,0 +1,31 @@
+<%
+
+my $dbh = dbh;
+#untaint svcnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+
+#my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$svcnum});
+#die "Unknown svcnum!" unless $svc_acct;
+
+my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$svcnum});
+die "Unknown svcnum!" unless $cust_svc;
+&eidiot(qq!This account has already been audited.  Cancel the 
+    <A HREF="!. popurl(2). qq!view/cust_pkg.cgi?! . $cust_svc->getfield('pkgnum') .
+    qq!pkgnum"> package</A> instead.!) 
+  if $cust_svc->pkgnum ne '' && $cust_svc->pkgnum ne '0';
+
+my $error = $cust_svc->cancel;
+
+if ( $error ) {
+  %>
+<!-- mason kludge -->
+<%
+  &eidiot($error);
+} else {
+  print $cgi->redirect(popurl(2));
+}
+
+%>
diff --git a/httemplate/misc/cancel_pkg.cgi b/httemplate/misc/cancel_pkg.cgi
new file mode 100755 (executable)
index 0000000..0487677
--- /dev/null
@@ -0,0 +1,15 @@
+<%
+
+#untaint pkgnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal pkgnum";
+my $pkgnum = $1;
+
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+
+my $error = $cust_pkg->cancel;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
+
+%>
diff --git a/httemplate/misc/catchall.cgi b/httemplate/misc/catchall.cgi
new file mode 100755 (executable)
index 0000000..3402b61
--- /dev/null
@@ -0,0 +1,133 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my($svc_domain, $svcnum, $pkgnum, $svcpart, $part_svc);
+if ( $cgi->param('error') ) {
+  $svc_domain = new FS::svc_domain ( {
+    map { $_, scalar($cgi->param($_)) } fields('svc_domain')
+  } );
+  $svcnum = $svc_domain->svcnum;
+  $pkgnum = $cgi->param('pkgnum');
+  $svcpart = $cgi->param('svcpart');
+  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+  die "No part_svc entry!" unless $part_svc;
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $svcnum=$1;
+    $svc_domain=qsearchs('svc_domain',{'svcnum'=>$svcnum})
+      or die "Unknown (svc_domain) svcnum!";
+
+    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+      or die "Unknown (cust_svc) svcnum!";
+
+    $pkgnum=$cust_svc->pkgnum;
+    $svcpart=$cust_svc->svcpart;
+  
+    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+    die "No part_svc entry!" unless $part_svc;
+
+  } else { 
+
+    die "Invalid (svc_domain) svcnum!";
+
+  }
+}
+
+my %email;
+if ($pkgnum) {
+
+  #find all possible user svcnums (and emails)
+
+  #starting with that currently attached
+  if ($svc_domain->catchall) {
+    my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$svc_domain->catchall});
+    $email{$svc_domain->catchall} = $svc_acct->email;
+  }
+
+  #and including the rest for this customer
+  my($u_part_svc,@u_acct_svcparts);
+  foreach $u_part_svc ( qsearch('part_svc',{'svcdb'=>'svc_acct'}) ) {
+    push @u_acct_svcparts,$u_part_svc->getfield('svcpart');
+  }
+
+  my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+  my($custnum)=$cust_pkg->getfield('custnum');
+  my($i_cust_pkg);
+  foreach $i_cust_pkg ( qsearch('cust_pkg',{'custnum'=>$custnum}) ) {
+    my($cust_pkgnum)=$i_cust_pkg->getfield('pkgnum');
+    my($acct_svcpart);
+    foreach $acct_svcpart (@u_acct_svcparts) {   #now find the corresponding 
+                                              #record(s) in cust_svc ( for this
+                                              #pkgnum ! )
+      my($i_cust_svc);
+      foreach $i_cust_svc ( qsearch('cust_svc',{'pkgnum'=>$cust_pkgnum,'svcpart'=>$acct_svcpart}) ) {
+        my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$i_cust_svc->getfield('svcnum')});
+        $email{$svc_acct->getfield('svcnum')}=$svc_acct->email;
+      }  
+    }
+  }
+
+} else {
+
+  my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$svc_domain->catchall});
+  $email{$svc_domain->catchall} = $svc_acct->email;
+}
+
+# add an absence of a catchall
+$email{''} = "(none)";
+
+my $p1 = popurl(1);
+print header("Domain Catchall Edit", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print qq!<FORM ACTION="${p1}process/catchall.cgi" METHOD=POST>!;
+
+#display
+
+       #formatting
+       print "<PRE>";
+
+#svcnum
+print qq!<INPUT TYPE="hidden" NAME="svcnum" VALUE="$svcnum">!;
+print qq!Service #<FONT SIZE=+1><B>!, $svcnum ? $svcnum : " (NEW)", "</B></FONT>";
+
+#pkgnum
+print qq!<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">!;
+#svcpart
+print qq!<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">!;
+
+my($domain,$catchall)=(
+  $svc_domain->domain,
+  $svc_domain->catchall,
+);
+
+print qq!<INPUT TYPE="hidden" NAME="domain" VALUE="$domain">!;
+
+#catchall
+print qq!\n\nMail to <I>(anything)</I>@<B>$domain</B> forwards to <SELECT NAME="catchall" SIZE=1>!;
+foreach $_ (keys %email) {
+  print "<OPTION", $_ eq $catchall ? " SELECTED" : "",
+        qq! VALUE="$_">$email{$_}!;
+}
+print "</SELECT>";
+
+       #formatting
+       print "</PRE>\n";
+
+print qq!<CENTER><INPUT TYPE="submit" VALUE="Submit"></CENTER>!;
+
+print <<END;
+
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/misc/change_pkg.cgi b/httemplate/misc/change_pkg.cgi
new file mode 100755 (executable)
index 0000000..5346fd9
--- /dev/null
@@ -0,0 +1,66 @@
+<!-- mason kludge -->
+<%
+
+my $pkgnum;
+if ( $cgi->param('error') ) {
+  #$custnum = $cgi->param('custnum');
+  #%remove_pkg = map { $_ => 1 } $cgi->param('remove_pkg');
+  $pkgnum = ($cgi->param('remove_pkg'))[0];
+} else {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/;
+  #$custnum = $1;
+  $pkgnum = $1;
+  #%remove_pkg = ();
+}
+
+my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } )
+  or die "unknown pkgnum $pkgnum";
+my $custnum = $cust_pkg->custnum;
+
+my $conf = new FS::Conf;
+
+my $p1 = popurl(1);
+
+my $cust_main = $cust_pkg->cust_main
+  or die "can't get cust_main record for custnum ". $cust_pkg->custnum.
+         " ( pkgnum ". cust_pkg->pkgnum. ")";
+my $agent = $cust_main->agent;
+
+print header("Change Package",  menubar(
+  "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+  'Main Menu' => $p,
+));
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT><BR><BR>"
+  if $cgi->param('error');
+
+my $part_pkg = $cust_pkg->part_pkg;
+
+print small_custview( $cust_main, $conf->config('countrydefault') ).
+      qq!<FORM ACTION="${p}edit/process/cust_pkg.cgi" METHOD=POST>!.
+      qq!<INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!.
+      qq!<INPUT TYPE="hidden" NAME="remove_pkg" VALUE="$pkgnum">!.
+      '<BR>Current package: '. $part_pkg->pkg. ' - '. $part_pkg->comment.
+      qq!<BR>New package: <SELECT NAME="new_pkgpart"><OPTION VALUE=0></OPTION>!;
+
+foreach my $part_pkg (
+  grep { ! $_->disabled && $_->pkgpart != $cust_pkg->pkgpart }
+    map { $_->part_pkg } $agent->agent_type->type_pkgs
+) {
+  my $pkgpart = $part_pkg->pkgpart;
+  print qq!<OPTION VALUE="$pkgpart"!;
+  print ' SELECTED' if $cgi->param('error')
+                       && $cgi->param('new_pkgpart') == $pkgpart;
+  print qq!>$pkgpart: !. $part_pkg->pkg. ' - '. $part_pkg->comment. '</OPTION>';
+}
+
+print <<END;
+</SELECT>
+<BR><BR><INPUT TYPE="submit" VALUE="Change package">
+    </FORM>
+  </BODY>
+</HTML>
+END
+%>
diff --git a/httemplate/misc/cust_main-cancel.cgi b/httemplate/misc/cust_main-cancel.cgi
new file mode 100755 (executable)
index 0000000..526e128
--- /dev/null
@@ -0,0 +1,16 @@
+<%
+
+#untaint custnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal custnum";
+my $custnum = $1;
+
+my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+
+my $error = $cust_main->cancel;
+eidiot($error) if $error;
+
+#print $cgi->redirect($p. "view/cust_main.cgi?". $cust_main->custnum);
+print $cgi->redirect($p);
+
+%>
diff --git a/httemplate/misc/cust_main-import.cgi b/httemplate/misc/cust_main-import.cgi
new file mode 100644 (file)
index 0000000..6b36f47
--- /dev/null
@@ -0,0 +1,51 @@
+<!-- mason kludge -->
+<%= header('Batch Customer Import') %>
+<FORM ACTION="process/cust_main-import.cgi" METHOD="post" ENCTYPE="multipart/form-data">
+Import a CSV file containing customer records.<BR><BR>
+Default file format is CSV, with the following field order: <i>cust_pkg.setup, dayphone, first, last, address1, address2, city, state, zip, comments</i><BR><BR>
+
+<%
+  #false laziness with edit/cust_main.cgi
+  my @agents = qsearch( 'agent', {} );
+  die "No agents created!" unless @agents;
+  my $agentnum = $agents[0]->agentnum; #default to first
+
+  if ( scalar(@agents) == 1 ) {
+%>
+    <INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
+<% } else { %>
+    <BR><BR>Agent <SELECT NAME="agentnum" SIZE="1">
+  <% foreach my $agent (sort { $a->agent cmp $b->agent } @agents) { %>
+    <OPTION VALUE="<%= $agent->agentnum %>" <%= " SELECTED"x($agent->agentnum==$agentnum) %>><%= $agent->agent %></OPTION>
+  <% } %>
+    </SELECT><BR><BR>
+<% } %>
+
+<%
+  my @referrals = qsearch('part_referral',{});
+  die "No advertising sources created!" unless @referrals;
+  my $refnum = $referrals[0]->refnum; #default to first
+
+  if ( scalar(@referrals) == 1 ) {
+%>
+    <INPUT TYPE="hidden" NAME="refnum" VALUE="<%= $refnum %>">
+<% } else { %>
+    <BR><BR>Advertising source <SELECT NAME="refnum" SIZE="1">
+  <% foreach my $referral ( sort { $a->referral <=> $b->referral } @referrals) { %>
+    <OPTION VALUE="<%= $referral->refnum %>" <%= " SELECTED"x($referral->refnum==$refnum) %>><%= $referral->refnum %>: <%= $referral->referral %></OPTION>
+  <% } %>
+    </SELECT><BR><BR>
+<% } %>
+
+    First package: <SELECT NAME="pkgpart"><OPTION VALUE="">(none)</OPTION>
+<% foreach my $part_pkg ( qsearch('part_pkg',{'disabled'=>'' }) ) { %>
+     <OPTION VALUE="<%= $part_pkg->pkgpart %>"><%= $part_pkg->pkg. ' - '. $part_pkg->comment %></OPTION>
+<% } %>
+</SELECT><BR><BR>
+
+    CSV Filename: <INPUT TYPE="file" NAME="csvfile"><BR><BR>
+    <INPUT TYPE="submit" VALUE="Import">
+    </FORM>
+  </BODY>
+<HTML>
+
diff --git a/httemplate/misc/cust_main-import_charges.cgi b/httemplate/misc/cust_main-import_charges.cgi
new file mode 100644 (file)
index 0000000..0822b9e
--- /dev/null
@@ -0,0 +1,14 @@
+<!-- mason kludge -->
+<%= header('Batch Customer Charge') %>
+<FORM ACTION="process/cust_main-import_charges.cgi" METHOD="post" ENCTYPE="multipart/form-data">
+Import a CSV file containing customer charges.<BR><BR>
+Default file format is CSV, with the following field order: <i>custnum, amount, description</i><BR><BR>
+If <i>amount</i> is negative, a credit will be applied instead.<BR><BR>
+<BR><BR>
+
+    CSV Filename: <INPUT TYPE="file" NAME="csvfile"><BR><BR>
+    <INPUT TYPE="submit" VALUE="Import">
+    </FORM>
+  </BODY>
+<HTML>
+
diff --git a/httemplate/misc/delete-cust_pay.cgi b/httemplate/misc/delete-cust_pay.cgi
new file mode 100755 (executable)
index 0000000..3efd918
--- /dev/null
@@ -0,0 +1,16 @@
+<%
+
+#untaint paynum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal paynum";
+my $paynum = $1;
+
+my $cust_pay = qsearchs('cust_pay',{'paynum'=>$paynum});
+my $custnum = $cust_pay->custnum;
+
+my $error = $cust_pay->delete;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "view/cust_main.cgi?". $custnum);
+
+%>
diff --git a/httemplate/misc/delete-customer.cgi b/httemplate/misc/delete-customer.cgi
new file mode 100755 (executable)
index 0000000..4302317
--- /dev/null
@@ -0,0 +1,60 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+die "Customer deletions not enabled" unless $conf->exists('deletecustomers');
+
+my($custnum, $new_custnum);
+if ( $cgi->param('error') ) {
+  $custnum = $cgi->param('custnum');
+  $new_custnum = $cgi->param('new_custnum');
+} else {
+  my($query) = $cgi->keywords;
+  $query =~ /^(\d+)$/ or die "Illegal query: $query";
+  $custnum = $1;
+  $new_custnum = '';
+}
+my $cust_main = qsearchs( 'cust_main', { 'custnum' => $custnum } )
+  or die "Customer not found: $custnum";
+
+print header('Delete customer');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+
+print 
+  qq!<form action="!, popurl(1), qq!process/delete-customer.cgi" method=post>!,
+  qq!<input type="hidden" name="custnum" value="$custnum">!;
+
+if ( qsearch('cust_pkg', { 'custnum' => $custnum, 'cancel' => '' } ) ) {
+  print "Move uncancelled packages to customer number ",
+        qq!<input type="text" name="new_custnum" value="$new_custnum"><br><br>!;
+}
+
+print <<END;
+This will <b>completely remove</b> all traces of this customer record.  This
+is <B>not</B> what you want if this is a real customer who has simply
+canceled service with you.  For that, cancel all of the customer's packages.
+(you can optionally hide cancelled customers with the <a href="../config/config-view.cgi#hidecancelledcustomers">hidecancelledcustomers</a> configuration option)
+<br>
+<br>Are you <b>absolutely sure</b> you want to delete this customer?
+<br><input type="submit" value="Yes">
+</form></body></html>
+END
+
+#Deleting a customer you have financial records on (i.e. credits) is
+#typically considered fraudulant bookkeeping.  Remember, deleting   
+#customers should ONLY be used for completely bogus records.  You should
+#NOT delete real customers who simply discontinue service.
+#
+#For real customers who simply discontinue service, cancel all of the
+#customer's packages.  Customers with all cancelled packages are not  
+#billed.  There is no need to take further action to prevent billing on
+#customers with all cancelled packages.
+#
+#Also see the "hidecancelledcustomers" and "hidecancelledpackages"
+#configuration options, which will allow you to surpress the display of
+#cancelled customers and packages, respectively.
+
+%>
diff --git a/httemplate/misc/delete-domain_record.cgi b/httemplate/misc/delete-domain_record.cgi
new file mode 100755 (executable)
index 0000000..dcc2d50
--- /dev/null
@@ -0,0 +1,15 @@
+<%
+
+#untaint recnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal recnum";
+my $recnum = $1;
+
+my $domain_record = qsearchs('domain_record',{'recnum'=>$recnum});
+
+my $error = $domain_record->delete;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "view/svc_domain.cgi?". $domain_record->svcnum);
+
+%>
diff --git a/httemplate/misc/delete-part_export.cgi b/httemplate/misc/delete-part_export.cgi
new file mode 100755 (executable)
index 0000000..7c4ab8b
--- /dev/null
@@ -0,0 +1,15 @@
+<%
+
+#untaint exportnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal exportnum";
+my $exportnum = $1;
+
+my $part_export = qsearchs('part_export',{'exportnum'=>$exportnum});
+
+my $error = $part_export->delete;
+eidiot($error) if $error;
+
+print $cgi->redirect($p. "browse/part_export.cgi");
+
+%>
diff --git a/httemplate/misc/expire_pkg.cgi b/httemplate/misc/expire_pkg.cgi
new file mode 100755 (executable)
index 0000000..9e4ce8b
--- /dev/null
@@ -0,0 +1,25 @@
+<%
+
+#untaint date & pkgnum
+
+my $date;
+if ( $cgi->param('date') ) {
+  str2time($cgi->param('date')) =~ /^(\d+)$/ or die "Illegal date";
+  $date=$1;
+} else {
+  $date='';
+}
+
+$cgi->param('pkgnum') =~ /^(\d+)$/ or die "Illegal pkgnum";
+my $pkgnum = $1;
+
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+my %hash = $cust_pkg->hash;
+$hash{expire}=$date;
+my $new = new FS::cust_pkg ( \%hash );
+my $error = $new->replace($cust_pkg);
+&eidiot($error) if $error;
+
+print $cgi->redirect(popurl(2). "view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
+
+%>
diff --git a/httemplate/misc/link.cgi b/httemplate/misc/link.cgi
new file mode 100755 (executable)
index 0000000..79adce8
--- /dev/null
@@ -0,0 +1,45 @@
+<!-- mason kludge -->
+<%
+
+my %link_field = (
+  'svc_acct'    => 'username',
+  'svc_domain'  => 'domain',
+  'svc_charge'  => '',
+  'svc_wo'      => '',
+);
+
+my($query) = $cgi->keywords;
+my($pkgnum, $svcpart) = ('', '');
+foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
+  $pkgnum=$1 if /^pkgnum(\d+)$/;
+  $svcpart=$1 if /^svcpart(\d+)$/;
+}
+
+my $part_svc = qsearchs('part_svc',{'svcpart'=>$svcpart});
+my $svc = $part_svc->getfield('svc');
+my $svcdb = $part_svc->getfield('svcdb');
+my $link_field = $link_field{$svcdb};
+
+print header("Link to existing $svc"),
+      qq!<FORM ACTION="!, popurl(1), qq!process/link.cgi" METHOD=POST>!;
+
+if ( $link_field ) { 
+  print <<END;
+  <INPUT TYPE="hidden" NAME="svcnum" VALUE="">
+  <INPUT TYPE="hidden" NAME="link_field" VALUE="$link_field">
+  $link_field of existing service: <INPUT TYPE="text" NAME="link_value">
+END
+} else {
+  print qq!Service # of existing service: <INPUT TYPE="text" NAME="svcnum" VALUE="">!;
+}
+
+print <<END;
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
+<INPUT TYPE="hidden" NAME="svcpart" VALUE="$svcpart">
+<P><CENTER><INPUT TYPE="submit" VALUE="Link"></CENTER>
+    </FORM>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/misc/meta-import.cgi b/httemplate/misc/meta-import.cgi
new file mode 100644 (file)
index 0000000..2f3b738
--- /dev/null
@@ -0,0 +1,64 @@
+<!-- mason kludge -->
+<%= header('Import') %>
+<FORM ACTION="process/meta-import.cgi" METHOD="post" ENCTYPE="multipart/form-data">
+Import data from a DBI data source<BR><BR>
+
+<%
+  #false laziness with edit/cust_main.cgi
+  my @agents = qsearch( 'agent', {} );
+  die "No agents created!" unless @agents;
+  my $agentnum = $agents[0]->agentnum; #default to first
+
+  if ( scalar(@agents) == 1 ) {
+%>
+    <INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
+<% } else { %>
+    <BR><BR>Agent <SELECT NAME="agentnum" SIZE="1">
+  <% foreach my $agent (sort { $a->agent cmp $b->agent } @agents) { %>
+    <OPTION VALUE="<%= $agent->agentnum %>" <%= " SELECTED"x($agent->agentnum==$agentnum) %>><%= $agent->agent %></OPTION>
+  <% } %>
+    </SELECT><BR><BR>
+<% } %>
+
+<%
+  my @referrals = qsearch('part_referral',{});
+  die "No advertising sources created!" unless @referrals;
+  my $refnum = $referrals[0]->refnum; #default to first
+
+  if ( scalar(@referrals) == 1 ) {
+%>
+    <INPUT TYPE="hidden" NAME="refnum" VALUE="<%= $refnum %>">
+<% } else { %>
+    <BR><BR>Advertising source <SELECT NAME="refnum" SIZE="1">
+  <% foreach my $referral ( sort { $a->referral <=> $b->referral } @referrals) { %>
+    <OPTION VALUE="<%= $referral->refnum %>" <%= " SELECTED"x($referral->refnum==$refnum) %>><%= $referral->refnum %>: <%= $referral->referral %></OPTION>
+  <% } %>
+    </SELECT><BR><BR>
+<% } %>
+
+    First package: <SELECT NAME="pkgpart"><OPTION VALUE="">(none)</OPTION>
+<% foreach my $part_pkg ( qsearch('part_pkg',{'disabled'=>'' }) ) { %>
+     <OPTION VALUE="<%= $part_pkg->pkgpart %>"><%= $part_pkg->pkg. ' - '. $part_pkg->comment %></OPTION>
+<% } %>
+</SELECT><BR><BR>
+
+  <table>
+    <tr>
+      <td align="right">DBI data source: </td>
+      <td><INPUT TYPE="text" NAME="data_source"></td>
+    </tr>
+    <tr>
+      <td align="right">DBI username: </td>
+      <td><INPUT TYPE="text" NAME="username"></td>
+    </tr>
+    <tr>
+      <td align="right">DBI password: </td>
+      <td><INPUT TYPE="text" NAME="password"></td>
+    </tr>
+  </table>
+  <INPUT TYPE="submit" VALUE="Import">
+
+  </FORM>
+  </BODY>
+<HTML>
+
diff --git a/httemplate/misc/print-invoice.cgi b/httemplate/misc/print-invoice.cgi
new file mode 100755 (executable)
index 0000000..a5500bf
--- /dev/null
@@ -0,0 +1,23 @@
+<%
+
+my $conf = new FS::Conf;
+my $lpr = $conf->config('lpr');
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d*)$/;
+my $invnum = $1;
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+die "Can't find invoice!\n" unless $cust_bill;
+
+        open(LPR,"|$lpr") or die "Can't open $lpr: $!";
+        print LPR $cust_bill->print_text; #( date )
+        close LPR
+          or die $! ? "Error closing $lpr: $!"
+                       : "Exit status $? from $lpr";
+
+my $custnum = $cust_bill->getfield('custnum');
+
+print $cgi->redirect(popurl(2). "view/cust_main.cgi?$custnum#history");
+
+%>
diff --git a/httemplate/misc/process/catchall.cgi b/httemplate/misc/process/catchall.cgi
new file mode 100755 (executable)
index 0000000..44a63f9
--- /dev/null
@@ -0,0 +1,33 @@
+<%
+
+$FS::svc_domain::whois_hack=1;
+
+$cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
+my $svcnum =$1;
+
+my $old = qsearchs('svc_domain',{'svcnum'=>$svcnum}) if $svcnum;
+
+my $new = new FS::svc_domain ( {
+  map {
+    ($_, scalar($cgi->param($_)));
+  } ( fields('svc_domain'), qw( pkgnum svcpart ) )
+} );
+
+$new->setfield('action' => 'M');
+
+my $error;
+if ( $svcnum ) {
+  $error = $new->replace($old);
+} else {
+  $error = $new->insert;
+  $svcnum = $new->getfield('svcnum');
+} 
+
+if ($error) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "catchall.cgi?". $cgi->query_string );
+} else {
+  print $cgi->redirect(popurl(3). "view/svc_domain.cgi?$svcnum");
+}
+
+%>
diff --git a/httemplate/misc/process/cust_main-import.cgi b/httemplate/misc/process/cust_main-import.cgi
new file mode 100644 (file)
index 0000000..9e1adce
--- /dev/null
@@ -0,0 +1,30 @@
+<%
+
+  my $fh = $cgi->upload('csvfile');
+  #warn $cgi;
+  #warn $fh;
+
+  my $error = defined($fh)
+    ? FS::cust_main::batch_import( {
+        filehandle => $fh,
+        agentnum   => scalar($cgi->param('agentnum')),
+        refnum     => scalar($cgi->param('refnum')),
+        pkgpart    => scalar($cgi->param('pkgpart')),
+        'fields'    => [qw( cust_pkg.setup dayphone first last address1 address2
+                           city state zip comments                          )],
+      } )
+    : 'No file';
+
+  if ( $error ) {
+    %>
+    <!-- mason kludge -->
+    <%
+    eidiot($error);
+#    $cgi->param('error', $error);
+#    print $cgi->redirect( "${p}cust_main-import.cgi
+  } else {
+    %>
+    <!-- mason kludge -->
+    <%= header('Import sucessful') %> <%
+  }
+%>
diff --git a/httemplate/misc/process/cust_main-import_charges.cgi b/httemplate/misc/process/cust_main-import_charges.cgi
new file mode 100644 (file)
index 0000000..14df1bd
--- /dev/null
@@ -0,0 +1,26 @@
+<%
+
+  my $fh = $cgi->upload('csvfile');
+  #warn $cgi;
+  #warn $fh;
+
+  my $error = defined($fh)
+    ? FS::cust_main::batch_charge( {
+        filehandle => $fh,
+        'fields'    => [qw( custnum amount pkg )],
+      } )
+    : 'No file';
+
+  if ( $error ) {
+    %>
+    <!-- mason kludge -->
+    <%
+    eidiot($error);
+#    $cgi->param('error', $error);
+#    print $cgi->redirect( "${p}cust_main-import_charges.cgi
+  } else {
+    %>
+    <!-- mason kludge -->
+    <%= header('Import sucessful') %> <%
+  }
+%>
diff --git a/httemplate/misc/process/delete-customer.cgi b/httemplate/misc/process/delete-customer.cgi
new file mode 100755 (executable)
index 0000000..16bdbae
--- /dev/null
@@ -0,0 +1,29 @@
+<%
+
+my $conf = new FS::Conf;
+die "Customer deletions not enabled" unless $conf->exists('deletecustomers');
+
+$cgi->param('custnum') =~ /^(\d+)$/;
+my $custnum = $1;
+my $new_custnum;
+if ( $cgi->param('new_custnum') ) {
+  $cgi->param('new_custnum') =~ /^(\d+)$/
+    or die "Illegal new customer number: ". $cgi->param('new_custnum');
+  $new_custnum = $1;
+} else {
+  $new_custnum = '';
+}
+my $cust_main = qsearchs( 'cust_main', { 'custnum' => $custnum } )
+  or die "Customer not found: $custnum";
+
+my $error = $cust_main->delete($new_custnum);
+
+if ( $error ) {
+  $cgi->param('error', $error);
+  print $cgi->redirect(popurl(2). "delete-customer.cgi?". $cgi->query_string );
+} elsif ( $new_custnum ) {
+  print $cgi->redirect(popurl(3). "view/cust_main.cgi?$new_custnum");
+} else {
+  print $cgi->redirect(popurl(3));
+}
+%>
diff --git a/httemplate/misc/process/link.cgi b/httemplate/misc/process/link.cgi
new file mode 100755 (executable)
index 0000000..5d80ade
--- /dev/null
@@ -0,0 +1,43 @@
+<%
+
+$cgi->param('pkgnum') =~ /^(\d+)$/;
+my $pkgnum = $1;
+$cgi->param('svcpart') =~ /^(\d+)$/;
+my $svcpart = $1;
+$cgi->param('svcnum') =~ /^(\d*)$/;
+my $svcnum = $1;
+
+unless ( $svcnum ) {
+  my $part_svc = qsearchs('part_svc',{'svcpart'=>$svcpart});
+  my $svcdb = $part_svc->getfield('svcdb');
+  $cgi->param('link_field') =~ /^(\w+)$/;
+  my $link_field = $1;
+  my $svc_x = ( grep { $_->cust_svc->svcpart == $svcpart } 
+                  qsearch( $svcdb, { $link_field => $cgi->param('link_value') })
+              )[0];
+  eidiot("$link_field not found!") unless $svc_x;
+  $svcnum = $svc_x->svcnum;
+}
+
+my $old = qsearchs('cust_svc',{'svcnum'=>$svcnum});
+die "svcnum not found!" unless $old;
+#die "svcnum $svcnum already linked to package ". $old->pkgnum if $old->pkgnum;
+my $new = new FS::cust_svc ({
+  'svcnum' => $svcnum,
+  'pkgnum' => $pkgnum,
+  'svcpart' => $svcpart,
+});
+
+my $error = $new->replace($old);
+
+unless ($error) {
+  #no errors, so let's view this customer.
+  print $cgi->redirect(popurl(3). "view/cust_pkg.cgi?$pkgnum");
+} else {
+%>
+<!-- mason kludge -->
+<%
+  idiot($error);
+}
+
+%>
diff --git a/httemplate/misc/process/meta-import.cgi b/httemplate/misc/process/meta-import.cgi
new file mode 100644 (file)
index 0000000..2939c8f
--- /dev/null
@@ -0,0 +1,178 @@
+<!-- mason kludge -->
+<%= header('Map tables') %>
+
+<SCRIPT>
+var gSafeOnload = new Array();
+var gSafeOnsubmit = new Array();
+window.onload = SafeOnload;
+function SafeAddOnLoad(f) {
+  gSafeOnload[gSafeOnload.length] = f;
+}
+function SafeOnload() {
+  for (var i=0;i<gSafeOnload.length;i++)
+    gSafeOnload[i]();
+}
+function SafeAddOnSubmit(f) {
+  gSafeOnsubmit[gSafeOnsubmit.length] = f;
+}
+function SafeOnsubmit() {
+  for (var i=0;i<gSafeOnsubmit.length;i++)
+    gSafeOnsubmit[i]();
+}
+</SCRIPT>
+
+<FORM NAME="OneTrueForm" METHOD="POST" ACTION="meta-import.cgi">
+
+<%
+  #use DBIx::DBSchema;
+  my $schema = new_native DBIx::DBSchema
+                 map { $cgi->param($_) } qw( data_source username password );
+  foreach my $field (qw( data_source username password )) { %>
+    <INPUT TYPE="hidden" NAME=<%= $field %> VALUE="<%= $cgi->param($field) %>">
+  <% }
+
+  my %schema;
+  use Tie::DxHash;
+  tie %schema, 'Tie::DxHash';
+  if ( $cgi->param('schema') ) {
+    my $schema_string = $cgi->param('schema');
+    %> <INPUT TYPE="hidden" NAME="schema" VALUE="<%=$schema_string%>"> <%
+    %schema = map { /^\s*(\w+)\s*=>\s*(\w+)\s*$/
+                      or die "guru meditation #420: $_";
+                    ( $1 => $2 );
+                  }
+              split( /\n/, $schema_string );
+  }
+
+  #first page
+  unless ( $cgi->param('magic') ) { %>
+
+    <INPUT TYPE="hidden" NAME="magic" VALUE="process">
+    <%= hashmaker('schema', [ $schema->tables ],
+                            [ grep !/^h_/, dbdef->tables ],  ) %>
+    <br><INPUT TYPE="submit" VALUE="done">
+    <%
+
+  #second page
+  } elsif ( $cgi->param('magic') eq 'process' ) { %>
+
+    <INPUT TYPE="hidden" NAME="magic" VALUE="process2">
+    <%
+
+    my %unique;
+    foreach my $table ( keys %schema ) {
+
+      my @from_columns = $schema->table($table)->columns;
+      my @fs_columns = dbdef->table($schema{$table})->columns;
+
+      %>
+      <%= hashmaker( $table.'__'.$unique{$table}++,
+                     \@from_columns => \@fs_columns,
+                     $table         =>  $schema{$table}, ) %>
+      <br><hr><br>
+      <%
+
+    }
+
+    %>
+    <br><INPUT TYPE="submit" VALUE="done">
+    <%
+
+  #third (results)
+  } elsif ( $cgi->param('magic') eq 'process2' ) {
+
+    print "<pre>\n";
+
+    my %unique;
+    foreach my $table ( keys %schema ) {
+      ( my $spaces = $table ) =~ s/./ /g;
+      print "'$table' => { 'table' => '$schema{$table}',\n".
+            #(length($table) x ' '). "         'map'   => {\n";
+            "$spaces        'map'   => {\n";
+      my %map = map { /^\s*(\w+)\s*=>\s*(\w+)\s*$/
+                         or die "guru meditation #420: $_";
+                       ( $1 => $2 );
+                     }
+                 split( /\n/, $cgi->param($table.'__'.$unique{$table}++) );
+      foreach ( keys %map ) {
+        print "$spaces                     '$_' => '$map{$_}',\n";
+      }
+      print "$spaces                   },\n";
+      print "$spaces      },\n";
+
+    }
+    print "\n</pre>";
+
+  } else {
+    warn "unrecognized magic: ". $cgi->param('magic');
+  }
+
+  %>
+</FORM>
+</BODY>
+</HTML>
+
+  <%
+  #hashmaker widget
+  sub hashmaker {
+    my($name, $from, $to, $labelfrom, $labelto) = @_;
+    $fromsize = scalar(@$from);
+    $tosize = scalar(@$to);
+    "<TABLE><TR><TH>$labelfrom</TH><TH>$labelto</TH></TR><TR><TD>".
+        qq!<SELECT NAME="${name}_from" SIZE=$fromsize>\n!.
+        join("\n", map { qq!<OPTION VALUE="$_">$_</OPTION>! } sort { $a cmp $b } @$from ).
+        "</SELECT>\n<BR>".
+      qq!<INPUT TYPE="button" VALUE="refill" onClick="repack_${name}_from()">!.
+      '</TD><TD>'.
+        qq!<SELECT NAME="${name}_to" SIZE=$tosize>\n!.
+        join("\n", map { qq!<OPTION VALUE="$_">$_</OPTION>! } sort { $a cmp $b } @$to ).
+        "</SELECT>\n<BR>".
+      qq!<INPUT TYPE="button" VALUE="refill" onClick="repack_${name}_to()">!.
+      '</TD></TR>'.
+      '<TR><TD COLSPAN=2>'.
+        qq!<INPUT TYPE="button" VALUE="map" onClick="toke_$name(this.form)">!.
+      '</TD></TR><TR><TD COLSPAN=2>'.
+      qq!<TEXTAREA NAME="$name" COLS=80 ROWS=8></TEXTAREA>!.
+      '</TD></TR></TABLE>'.
+      "<script>
+            function toke_$name() {
+              fromObject = document.OneTrueForm.${name}_from;
+              for (var i=fromObject.options.length-1;i>-1;i--) {
+                if (fromObject.options[i].selected)
+                  fromname = deleteOption_$name(fromObject,i);
+              }
+              toObject = document.OneTrueForm.${name}_to;
+              for (var i=toObject.options.length-1;i>-1;i--) {
+                if (toObject.options[i].selected)
+                  toname = deleteOption_$name(toObject,i);
+              }
+              document.OneTrueForm.$name.value = document.OneTrueForm.$name.value + fromname + ' => ' + toname + '\\n';
+            }
+            function deleteOption_$name(object,index) {
+              value = object.options[index].value;
+              object.options[index] = null;
+              return value;
+            }
+            function repack_${name}_from() {
+              var object = document.OneTrueForm.${name}_from;
+              object.options.length = 0;
+              ". join("\n", 
+                   map { "addOption_$name(object, '$_');\n" }
+                       ( sort { $a cmp $b } @$from )           ). "
+            }
+            function repack_${name}_to() {
+              var object = document.OneTrueForm.${name}_to;
+              object.options.length = 0;
+              ". join("\n", 
+                   map { "addOption_$name(object, '$_');\n" }
+                       ( sort { $a cmp $b } @$to )           ). "
+            }
+            function addOption_$name(object,value) {
+              var length = object.length;
+              object.options[length] = new Option(value, value, false, false);
+            }
+      </script>".
+      '';
+  }
+
+%>
diff --git a/httemplate/misc/queue.cgi b/httemplate/misc/queue.cgi
new file mode 100644 (file)
index 0000000..ce9c8fb
--- /dev/null
@@ -0,0 +1,47 @@
+<%
+
+$cgi->param('action') =~ /^(new|del|(retry|remove) selected)$/
+  or die "Illegal action";
+my $action = $1;
+
+my $job;
+if ( $action eq 'new' || $action eq 'del' ) {
+  $cgi->param('jobnum') =~ /^(\d+)$/ or die "Illegal jobnum";
+  my $jobnum = $1;
+  $job = qsearchs('queue', { 'jobnum' => $1 })
+    or die "unknown jobnum $jobnum - ".
+           "it probably completed normally or was removed by another user";
+}
+
+if ( $action eq 'new' ) {
+  my %hash = $job->hash;
+  $hash{'status'} = 'new';
+  $hash{'statustext'} = '';
+  my $new = new FS::queue \%hash;
+  my $error = $new->replace($job);
+  die $error if $error;
+} elsif ( $action eq 'del' ) {
+  my $error = $job->delete;
+  die $error if $error;
+} elsif ( $action =~ /^(retry|remove) selected$/ ) {
+  foreach my $jobnum (
+    map { /^jobnum(\d+)$/; $1; } grep /^jobnum\d+$/, $cgi->param
+  ) {
+    my $job = qsearchs('queue', { 'jobnum' => $jobnum });
+    if ( $action eq 'retry selected' && $job ) { #new
+      my %hash = $job->hash;
+      $hash{'status'} = 'new';
+      $hash{'statustext'} = '';
+      my $new = new FS::queue \%hash;
+      my $error = $new->replace($job);
+      die $error if $error;
+    } elsif ( $action eq 'remove selected' && $job ) { #del
+      my $error = $job->delete;
+      die $error if $error;
+    }
+  }
+}
+
+print $cgi->redirect(popurl(2). "browse/queue.cgi");
+
+%>
diff --git a/httemplate/misc/susp_pkg.cgi b/httemplate/misc/susp_pkg.cgi
new file mode 100755 (executable)
index 0000000..4a19fa8
--- /dev/null
@@ -0,0 +1,15 @@
+<%
+
+#untaint pkgnum
+my ($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal pkgnum";
+my $pkgnum = $1;
+
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+
+my $error = $cust_pkg->suspend;
+&eidiot($error) if $error;
+
+print $cgi->redirect(popurl(2). "view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
+
+%>
diff --git a/httemplate/misc/unapply-cust_pay.cgi b/httemplate/misc/unapply-cust_pay.cgi
new file mode 100755 (executable)
index 0000000..28643ef
--- /dev/null
@@ -0,0 +1,18 @@
+<%
+
+#untaint paynum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal paynum";
+my $paynum = $1;
+
+my $cust_pay = qsearchs('cust_pay', { 'paynum' => $paynum } );
+my $custnum = $cust_pay->custnum;
+
+foreach my $cust_bill_pay ( $cust_pay->cust_bill_pay ) {
+  my $error = $cust_bill_pay->delete;
+  eidiot($error) if $error;
+}
+
+print $cgi->redirect($p. "view/cust_main.cgi?". $custnum);
+
+%>
diff --git a/httemplate/misc/unprovision.cgi b/httemplate/misc/unprovision.cgi
new file mode 100755 (executable)
index 0000000..8f2a7d1
--- /dev/null
@@ -0,0 +1,33 @@
+<%
+
+my $dbh = dbh;
+#untaint svcnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+
+#my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$svcnum});
+#die "Unknown svcnum!" unless $svc_acct;
+
+my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$svcnum});
+die "Unknown svcnum!" unless $cust_svc;
+#&eidiot(qq!This account has already been audited.  Cancel the 
+#    <A HREF="!. popurl(2). qq!view/cust_pkg.cgi?! . $cust_svc->getfield('pkgnum') .
+#    qq!pkgnum"> package</A> instead.!) 
+#  if $cust_svc->pkgnum ne '' && $cust_svc->pkgnum ne '0';
+
+my $custnum = $cust_svc->cust_pkg->custnum;
+
+my $error = $cust_svc->cancel;
+
+if ( $error ) {
+  %>
+<!-- mason kludge -->
+<%
+  &eidiot($error);
+} else {
+  print $cgi->redirect(popurl(2)."view/cust_main.cgi?$custnum");
+}
+
+%>
diff --git a/httemplate/misc/unsusp_pkg.cgi b/httemplate/misc/unsusp_pkg.cgi
new file mode 100755 (executable)
index 0000000..5008729
--- /dev/null
@@ -0,0 +1,15 @@
+<%
+
+#untaint pkgnum
+my ($query) = $cgi->keywords;
+$query =~ /^(\d+)$/ || die "Illegal pkgnum";
+my $pkgnum = $1;
+
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+
+my $error = $cust_pkg->unsuspend;
+&eidiot($error) if $error;
+
+print $cgi->redirect(popurl(2). "view/cust_main.cgi?".$cust_pkg->getfield('custnum'));
+
+%>
diff --git a/httemplate/search/cust_bill.cgi b/httemplate/search/cust_bill.cgi
new file mode 100755 (executable)
index 0000000..985e3db
--- /dev/null
@@ -0,0 +1,165 @@
+<%
+
+my $conf = new FS::Conf;
+my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+my $orderby = ''; #removeme
+
+my $limit = '';
+$limit .= "LIMIT $maxrecords" if $maxrecords;
+
+my $offset = $cgi->param('offset') || 0;
+$limit .= " OFFSET $offset" if $offset;
+
+my($total, $tot_amount, $tot_balance);
+
+my(@cust_bill);
+if ( $cgi->keywords ) {
+  my($query) = $cgi->keywords;
+  my $owed = "charged - ( select coalesce(sum(amount),0) from cust_bill_pay
+                          where cust_bill_pay.invnum = cust_bill.invnum )
+                      - ( select coalesce(sum(amount),0) from cust_credit_bill
+                          where cust_credit_bill.invnum = cust_bill.invnum )";
+  my @where;
+  if ( $query =~ /^(OPEN(\d*)_)?(invnum|date|custnum)$/ ) {
+    my($open, $days, $field) = ($1, $2, $3);
+    $field = "_date" if $field eq 'date';
+    $orderby = "ORDER BY cust_bill.$field";
+    push @where, "0 != $owed" if $open;
+    push @where, "cust_bill._date < ". (time-86400*$days) if $days;
+  } else {
+    die "unknown query string $query";
+  }
+
+  my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
+
+  my $statement = "SELECT COUNT(*), sum(charged), sum($owed)
+                   FROM cust_bill $extra_sql";
+  my $sth = dbh->prepare($statement) or die dbh->errstr. " doing $statement";
+  $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+
+  ( $total, $tot_amount, $tot_balance ) = @{$sth->fetchrow_arrayref};
+
+  @cust_bill = qsearch(
+    'cust_bill',
+    {},
+    "cust_bill.*, $owed as owed",
+    "$extra_sql $orderby $limit"
+  );
+} else {
+  $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/;
+  my $invnum = $2;
+  @cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum } );
+  $total = scalar(@cust_bill);
+}
+
+#if ( scalar(@cust_bill) == 1 ) {
+if ( $total == 1 ) {
+  my $invnum = $cust_bill[0]->invnum;
+  print $cgi->redirect(popurl(2). "view/cust_bill.cgi?$invnum");  #redirect
+} elsif ( scalar(@cust_bill) == 0 ) {
+%>
+<!-- mason kludge -->
+<%
+  eidiot("Invoice not found.");
+} else {
+%>
+<!-- mason kludge -->
+<%
+
+  #begin pager
+  my $pager = '';
+  if ( $total != scalar(@cust_bill) && $maxrecords ) {
+    unless ( $offset == 0 ) {
+      $cgi->param('offset', $offset - $maxrecords);
+      $pager .= '<A HREF="'. $cgi->self_url.
+                '"><B><FONT SIZE="+1">Previous</FONT></B></A> ';
+    }
+    my $poff;
+    my $page;
+    for ( $poff = 0; $poff < $total; $poff += $maxrecords ) {
+      $page++;
+      if ( $offset == $poff ) {
+        $pager .= qq!<FONT SIZE="+2">$page</FONT> !;
+      } else {
+        $cgi->param('offset', $poff);
+        $pager .= qq!<A HREF="!. $cgi->self_url. qq!">$page</A> !;
+      }
+    }
+    unless ( $offset + $maxrecords > $total ) {
+      $cgi->param('offset', $offset + $maxrecords);
+      $pager .= '<A HREF="'. $cgi->self_url.
+                '"><B><FONT SIZE="+1">Next</FONT></B></A> ';
+    }
+  }
+  #end pager
+
+  print header("Invoice Search Results", menubar(
+          'Main Menu', popurl(2)
+        )).
+        "$total matching invoices found<BR>".
+        "\$$tot_balance total balance<BR>".
+        "\$$tot_amount total amount<BR>".
+        "<BR>$pager". table(). <<END;
+      <TR>
+        <TH></TH>
+        <TH>Balance</TH>
+        <TH>Amount</TH>
+        <TH>Date</TH>
+        <TH>Contact name</TH>
+        <TH>Company</TH>
+      </TR>
+END
+
+  foreach my $cust_bill ( @cust_bill ) {
+    my($invnum, $owed, $charged, $date ) = (
+      $cust_bill->invnum,
+      sprintf("%.2f", $cust_bill->getfield('owed')),
+      sprintf("%.2f", $cust_bill->charged),
+      $cust_bill->_date,
+    );
+    my $pdate = time2str("%b %d %Y", $date);
+
+    my $rowspan = 1;
+
+    my $view = popurl(2). "view/cust_bill.cgi?$invnum";
+    print <<END;
+      <TR>
+        <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$invnum</FONT></A></TD>
+        <TD ROWSPAN=$rowspan ALIGN="right"><A HREF="$view"><FONT SIZE=-1>\$$owed</FONT></A></TD>
+        <TD ROWSPAN=$rowspan ALIGN="right"><A HREF="$view"><FONT SIZE=-1>\$$charged</FONT></A></TD>
+        <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$pdate</FONT></A></TD>
+END
+    my $custnum = $cust_bill->custnum;
+    my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+    if ( $cust_main ) {
+      my $cview = popurl(2). "view/cust_main.cgi?". $cust_main->custnum;
+      my ( $name, $company ) = (
+        $cust_main->last. ', '. $cust_main->first,
+        $cust_main->company,
+      );
+      print <<END;
+        <TD ROWSPAN=$rowspan><A HREF="$cview"><FONT SIZE=-1>$name</FONT></A></TD>
+        <TD ROWSPAN=$rowspan><A HREF="$cview"><FONT SIZE=-1>$company</FONT></A></TD>
+END
+    } else {
+      print <<END
+        <TD ROWSPAN=$rowspan COLSPAN=2>WARNING: couldn't find cust_main.custnum $custnum (cust_bill.invnum $invnum)</TD>
+END
+    }
+
+    print "</TR>";
+  }
+  $tot_balance = sprintf("%.2f", $tot_balance);
+  $tot_amount = sprintf("%.2f", $tot_amount);
+  print "</TABLE>$pager<BR>". table(). <<END;
+      <TR><TD>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</TD><TH><FONT SIZE=-1>Total<BR>Balance</FONT></TH><TH><FONT SIZE=-1>Total<BR>Amount</FONT></TH></TR>
+      <TR><TD></TD><TD ALIGN="right"><FONT SIZE=-1>\$$tot_balance</FONT></TD><TD ALIGN="right"><FONT SIZE=-1>\$$tot_amount</FONT></TD></TD></TR>
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+}
+
+%>
diff --git a/httemplate/search/cust_bill.html b/httemplate/search/cust_bill.html
new file mode 100755 (executable)
index 0000000..36e8bc9
--- /dev/null
@@ -0,0 +1,19 @@
+<HTML>
+  <HEAD>
+    <TITLE>Invoice Search</TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <FONT SIZE=7>
+      Invoice Search
+    </FONT>
+    <BR><BR>
+    <FORM ACTION="cust_bill.cgi" METHOD="post">
+      Search for <B>invoice #</B>: 
+      <INPUT TYPE="text" NAME="invnum">
+
+      <P><INPUT TYPE="submit" VALUE="Search">
+
+    </FORM>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_bill_event.cgi b/httemplate/search/cust_bill_event.cgi
new file mode 100644 (file)
index 0000000..b76f66b
--- /dev/null
@@ -0,0 +1,62 @@
+<!-- mason kludge -->
+<%
+
+#false laziness with view/cust_bill.cgi
+
+$cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/;
+my $beginning = str2time($1) || 0;
+
+$cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/;
+my $ending = str2time($1) + 86400;
+
+my @cust_bill_event =
+  sort { $a->_date <=> $b->_date }
+    qsearch('cust_bill_event', {
+      _date => { op=> '>=', value=>$beginning },
+      statustext => { op=> '!=', value=>'' },
+# i wish...
+#      _date => { op=> '<=', value=>$ending },
+    }, '', "AND _date <= $ending");
+
+%>
+
+<%= header('Failed billing events') %>
+
+<%= table() %>
+<TR>
+  <TH>Event</TH>
+  <TH>Date</TH>
+  <TH>Status</TH>
+  <TH>Invoice</TH>
+  <TH>(bill) name</TH>
+  <TH>company</TH>
+<% if ( defined dbdef->table('cust_main')->column('ship_last') ) { %>
+  <TH>(service) name</TH>
+  <TH>company</TH>
+<% } %>
+</TR>
+
+<% foreach my $cust_bill_event ( @cust_bill_event ) {
+   my $status = $cust_bill_event->status;
+   $status .= ': '.$cust_bill_event->statustext if $cust_bill_event->statustext;
+   my $cust_bill = $cust_bill_event->cust_bill;
+   my $cust_main = $cust_bill->cust_main;
+   my $invlink = "${p}view/cust_bill.cgi?". $cust_bill->invnum;
+   my $custlink = "${p}view/cust_main.cgi?". $cust_main->custnum;
+%>
+<TR>
+  <TD><%= $cust_bill_event->part_bill_event->event %></TD>
+  <TD><%= time2str("%a %b %e %T %Y", $cust_bill_event->_date) %></TD>
+  <TD><%= $status %></TD>
+  <TD><A HREF="<%=$invlink%>">Invoice #<%= $cust_bill->invnum %> (<%= time2str("%D", $cust_bill->_date ) %>)</A></TD>
+  <TD><A HREF="<%=$custlink%>"><%= $cust_main->last. ', '. $cust_main->first %></A></TD>
+  <TD><A HREF="<%=$custlink%>"><%= $cust_main->company %></A></TD>
+  <% if ( defined dbdef->table('cust_main')->column('ship_last') ) { %>
+    <TD><A HREF="<%=$custlink%>"><%= $cust_main->ship_last. ', '. $cust_main->ship_first %></A></TD>
+    <TD><A HREF="<%=$custlink%>"><%= $cust_main->ship_company %></A></TD>
+  <% } %>
+</TR>
+<% } %>
+</TABLE>
+
+</BODY></HTML>
diff --git a/httemplate/search/cust_bill_event.html b/httemplate/search/cust_bill_event.html
new file mode 100755 (executable)
index 0000000..d76ce3c
--- /dev/null
@@ -0,0 +1,23 @@
+<HTML>
+  <HEAD>
+    <TITLE>Failed billing events</TITLE>
+  </HEAD>
+  <BODY>
+    <CENTER>
+      <H1>Failed billing events</H1>
+    </CENTER>
+    <HR>
+    <FORM ACTION="cust_bill_event.cgi" METHOD="post">
+      Return <B>failed billing events</B> for period: 
+      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
+      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
+
+      <P><INPUT TYPE="submit" VALUE="Get Report">
+
+    </FORM>
+
+  <HR>
+
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_main-otaker.cgi b/httemplate/search/cust_main-otaker.cgi
new file mode 100755 (executable)
index 0000000..b7173c4
--- /dev/null
@@ -0,0 +1,29 @@
+<HTML>
+  <HEAD>
+    <TITLE>Customer Search</TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <FONT SIZE=7>
+      Customer Search
+    </FONT>
+    <BR>
+    <FORM ACTION="cust_main.cgi" METHOD="post">
+      Search for <B>Order taker</B>: 
+      <INPUT TYPE="hidden" NAME="otaker_on" VALUE="TRUE">
+      <% my $dbh = dbh;
+         my $sth = dbh->prepare("SELECT DISTINCT otaker FROM cust_main")
+           or eidiot $dbh->errstr;
+         $sth->execute() or eidiot $sth->errstr;
+#         my @otakers = map { $_->[0] } @{$sth->selectall_arrayref};
+      %>
+      <SELECT NAME="otaker">
+      <% my $otaker; while ( $otaker = $sth->fetchrow_arrayref ) { %>
+        <OPTION><%= $otaker->[0] %></OTAKER>
+      <% } %>
+      </SELECT>
+      <P><INPUT TYPE="submit" VALUE="Search">
+
+    </FORM>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_main-payinfo.html b/httemplate/search/cust_main-payinfo.html
new file mode 100755 (executable)
index 0000000..671b5ef
--- /dev/null
@@ -0,0 +1,20 @@
+<HTML>
+  <HEAD>
+    <TITLE>Customer Search</TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <FONT SIZE=7>
+      Customer Search
+    </FONT>
+    <BR>
+    <FORM ACTION="cust_main.cgi" METHOD="post">
+      Search for <B>Credit card #</B>: 
+      <INPUT TYPE="hidden" NAME="card_on" VALUE="TRUE">
+      <INPUT TYPE="text" NAME="card">
+
+      <P><INPUT TYPE="submit" VALUE="Search">
+
+    </FORM>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_main-quickpay.html b/httemplate/search/cust_main-quickpay.html
new file mode 100755 (executable)
index 0000000..9f39db9
--- /dev/null
@@ -0,0 +1,43 @@
+<HTML>
+  <HEAD>
+    <TITLE>Quick payment entry</TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <FONT SIZE=7>
+      Quick payment entry
+    </FONT>
+    <BR><BR>
+    <FORM ACTION="cust_main.cgi" METHOD="post">
+      <INPUT TYPE="hidden" NAME="quickpay" VALUE="yes">
+      <INPUT TYPE="checkbox" NAME="last_on" CHECKED> Search for <B>last name</B>: 
+      <INPUT TYPE="text" NAME="last_text">
+      using search method: <SELECT NAME="last_type">
+        <OPTION SELECTED>All
+        <OPTION>Fuzzy
+        <OPTION>Substring
+        <OPTION>Exact
+      </SELECT>
+
+      <P><INPUT TYPE="checkbox" NAME="company_on" CHECKED> Search for <B>company</B>: 
+      <INPUT TYPE="text" NAME="company_text">
+      using search methods: <SELECT NAME="company_type">
+        <OPTION SELECTED>All
+        <OPTION>Fuzzy
+        <OPTION>Substring
+        <OPTION>Exact
+      </SELECT>
+
+      <P><INPUT TYPE="submit" VALUE="Search"> Note: Fuzzy searching can take a while.  Please be patient.
+
+    </FORM>
+
+  <HR>Explanation of search methods:
+  <UL>
+    <LI><B>All</B> - Try all search methods.
+    <LI><B>Fuzzy</B> - Searches for matches that are close to your text.
+    <LI><B>Substring</B> - Searches for matches that contain your text.
+    <LI><B>Exact</B> - Finds exact matches only, but much faster than the other search methods.
+  </UL>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_main.cgi b/httemplate/search/cust_main.cgi
new file mode 100755 (executable)
index 0000000..5b39a09
--- /dev/null
@@ -0,0 +1,668 @@
+<%
+
+my $conf = new FS::Conf;
+my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+#my $cache;
+
+#my $monsterjoin = <<END;
+#cust_main left outer join (
+#  ( cust_pkg left outer join part_pkg using(pkgpart)
+#  ) left outer join (
+#    (
+#      (
+#        ( cust_svc left outer join part_svc using (svcpart)
+#        ) left outer join svc_acct using (svcnum)
+#      ) left outer join svc_domain using(svcnum)
+#    ) left outer join svc_forward using(svcnum)
+#  ) using (pkgnum)
+#) using (custnum)
+#END
+
+#my $monsterjoin = <<END;
+#cust_main left outer join (
+#  ( cust_pkg left outer join part_pkg using(pkgpart)
+#  ) left outer join (
+#    (
+#      (
+#        ( cust_svc left outer join part_svc using (svcpart)
+#        ) left outer join (
+#          svc_acct left outer join (
+#            select svcnum, domain, catchall from svc_domain
+#            ) as svc_acct_domsvc (
+#              svc_acct_svcnum, svc_acct_domain, svc_acct_catchall
+#          ) on svc_acct.domsvc = svc_acct_domsvc.svc_acct_svcnum
+#        ) using (svcnum)
+#      ) left outer join svc_domain using(svcnum)
+#    ) left outer join svc_forward using(svcnum)
+#  ) using (pkgnum)
+#) using (custnum)
+#END
+
+my $limit = '';
+$limit .= "LIMIT $maxrecords" if $maxrecords;
+
+my $offset = $cgi->param('offset') || 0;
+$limit .= " OFFSET $offset" if $offset;
+
+my $total = 0;
+
+my(@cust_main, $sortby, $orderby);
+if ( $cgi->param('browse')
+     || $cgi->param('otaker_on')
+) {
+
+  my %search = ();
+  if ( $cgi->param('browse') ) {
+    my $query = $cgi->param('browse');
+    if ( $query eq 'custnum' ) {
+      $sortby=\*custnum_sort;
+      $orderby = "ORDER BY custnum";
+    } elsif ( $query eq 'last' ) {
+      $sortby=\*last_sort;
+      $orderby = "ORDER BY LOWER(last || ' ' || first)";
+    } elsif ( $query eq 'company' ) {
+      $sortby=\*company_sort;
+      $orderby = "ORDER BY LOWER(company || ' ' || last || ' ' || first )";
+    } else {
+      die "unknown browse field $query";
+    }
+  } else {
+    $sortby = \*last_sort; #??
+    $orderby = "ORDER BY LOWER(last || ' ' || first)"; #??
+    if ( $cgi->param('otaker_on') ) {
+      $cgi->param('otaker') =~ /^(\w{1,32})$/ or eidiot "Illegal otaker\n";
+      $search{otaker} = $1;
+    } else {
+      die "unknown query...";
+    }
+  }
+
+  my $ncancelled = '';
+
+  if ( driver_name eq 'mysql' ) {
+
+       my $sql = "CREATE TEMPORARY TABLE temp1_$$ TYPE=MYISAM
+                    SELECT cust_pkg.custnum,COUNT(*) as count
+                      FROM cust_pkg,cust_main
+                        WHERE cust_pkg.custnum = cust_main.custnum
+                              AND ( cust_pkg.cancel IS NULL
+                                    OR cust_pkg.cancel = 0 )
+                        GROUP BY cust_pkg.custnum";
+       my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
+       $sth->execute or die "Error executing \"$sql\": ". $sth->errstr;
+       $sql = "CREATE TEMPORARY TABLE temp2_$$ TYPE=MYISAM
+                 SELECT cust_pkg.custnum,COUNT(*) as count
+                   FROM cust_pkg,cust_main
+                     WHERE cust_pkg.custnum = cust_main.custnum
+                     GROUP BY cust_pkg.custnum";
+       $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
+       $sth->execute or die "Error executing \"$sql\": ". $sth->errstr;
+  }
+
+  if (  $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me
+       || ( $conf->exists('hidecancelledcustomers')
+             && ! $cgi->param('showcancelledcustomers') )
+     ) {
+    #grep { $_->ncancelled_pkgs || ! $_->all_pkgs }
+    if ( driver_name eq 'mysql' ) {
+       $ncancelled = "
+          temp1_$$.custnum = cust_main.custnum
+               AND temp2_$$.custnum = cust_main.custnum
+               AND (temp1_$$.count > 0
+                       OR temp2_$$.count = 0 )
+       ";
+    } else {
+       $ncancelled = "
+          0 < ( SELECT COUNT(*) FROM cust_pkg
+                       WHERE cust_pkg.custnum = cust_main.custnum
+                         AND ( cust_pkg.cancel IS NULL
+                               OR cust_pkg.cancel = 0
+                             )
+                   )
+            OR 0 = ( SELECT COUNT(*) FROM cust_pkg
+                       WHERE cust_pkg.custnum = cust_main.custnum
+                   )
+       ";
+    }
+
+  }
+
+  #EWWWWWW
+  my $qual = join(' AND ',
+            map { "$_ = ". dbh->quote($search{$_}) } keys %search );
+
+  if ( $ncancelled ) {
+    $qual .= ' AND ' if $qual;
+    $qual .= $ncancelled;
+  }
+    
+  $qual = " WHERE $qual" if $qual;
+  my $statement;
+  if ( driver_name eq 'mysql' ) {
+    $statement = "SELECT COUNT(*) FROM cust_main";
+    $statement .= ", temp1_$$, temp2_$$ $qual" if $qual;
+  } else {
+    $statement = "SELECT COUNT(*) FROM cust_main $qual";
+  }
+  my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
+  $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+
+  $total = $sth->fetchrow_arrayref->[0];
+
+  if ( $ncancelled ) {
+    if ( %search ) {
+      $ncancelled = " AND $ncancelled";
+    } else {
+      $ncancelled = " WHERE $ncancelled";
+    }
+  }
+
+  my @just_cust_main;
+  if ( driver_name eq 'mysql' ) {
+    @just_cust_main = qsearch('cust_main', \%search, 'cust_main.*',
+                              ",temp1_$$,temp2_$$ $ncancelled $orderby $limit");
+  } else {
+    @just_cust_main = qsearch('cust_main', \%search, '',   
+                              "$ncancelled $orderby $limit" );
+  }
+  if ( driver_name eq 'mysql' ) {
+    my $sql = "DROP TABLE temp1_$$,temp2_$$;";
+    my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
+    $sth->execute or die "Error executing \"$sql\": ". $sth->errstr;
+  }
+  @cust_main = @just_cust_main;
+
+#  foreach my $cust_main ( @just_cust_main ) {
+#
+#    my @one_cust_main;
+#    $FS::Record::DEBUG=1;
+#    ( $cache, @one_cust_main ) = jsearch(
+#      "$monsterjoin",
+#      { 'custnum' => $cust_main->custnum },
+#      '',
+#      '',
+#      'cust_main',
+#      'custnum',
+#    );
+#    push @cust_main, @one_cust_main;
+#  }
+
+} else {
+  @cust_main=();
+  $sortby = \*last_sort;
+
+  push @cust_main, @{&custnumsearch}
+    if $cgi->param('custnum_on') && $cgi->param('custnum_text');
+  push @cust_main, @{&cardsearch}
+    if $cgi->param('card_on') && $cgi->param('card');
+  push @cust_main, @{&lastsearch}
+    if $cgi->param('last_on') && $cgi->param('last_text');
+  push @cust_main, @{&companysearch}
+    if $cgi->param('company_on') && $cgi->param('company_text');
+  push @cust_main, @{&address2search}
+    if $cgi->param('address2_on') && $cgi->param('address2_text');
+  push @cust_main, @{&phonesearch}
+    if $cgi->param('phone_on') && $cgi->param('phone_text');
+  push @cust_main, @{&referralsearch}
+    if $cgi->param('referral_custnum');
+
+  if ( $cgi->param('company_on') && $cgi->param('company_text') ) {
+    $sortby = \*company_sort;
+    push @cust_main, @{&companysearch};
+  }
+
+  @cust_main = grep { $_->ncancelled_pkgs || ! $_->all_pkgs } @cust_main
+    if $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me
+       || ( $conf->exists('hidecancelledcustomers')
+             && ! $cgi->param('showcancelledcustomers') );
+
+  my %saw = ();
+  @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
+}
+
+my %all_pkgs;
+if ( $conf->exists('hidecancelledpackages' ) ) {
+  %all_pkgs = map { $_->custnum => [ $_->ncancelled_pkgs ] } @cust_main;
+} else {
+  %all_pkgs = map { $_->custnum => [ $_->all_pkgs ] } @cust_main;
+}
+#%all_pkgs = ();
+
+if ( scalar(@cust_main) == 1 && ! $cgi->param('referral_custnum') ) {
+  if ( $cgi->param('quickpay') eq 'yes' ) {
+    print $cgi->redirect(popurl(2). "edit/cust_pay.cgi?quickpay=yes;custnum=". $cust_main[0]->custnum);
+  } else {
+    print $cgi->redirect(popurl(2). "view/cust_main.cgi?". $cust_main[0]->custnum);
+  }
+  #exit;
+} elsif ( scalar(@cust_main) == 0 ) {
+%>
+<!-- mason kludge -->
+<%
+  eidiot "No matching customers found!\n";
+} else { 
+%>
+<!-- mason kludge -->
+<%
+
+  $total ||= scalar(@cust_main);
+  print header("Customer Search Results",menubar(
+    'Main Menu', popurl(2)
+  )), "$total matching customers found ";
+
+  #begin pager
+  my $pager = '';
+  if ( $total != scalar(@cust_main) && $maxrecords ) {
+    unless ( $offset == 0 ) {
+      $cgi->param('offset', $offset - $maxrecords);
+      $pager .= '<A HREF="'. $cgi->self_url.
+                '"><B><FONT SIZE="+1">Previous</FONT></B></A> ';
+    }
+    my $poff;
+    my $page;
+    for ( $poff = 0; $poff < $total; $poff += $maxrecords ) {
+      $page++;
+      if ( $offset == $poff ) {
+        $pager .= qq!<FONT SIZE="+2">$page</FONT> !;
+      } else {
+        $cgi->param('offset', $poff);
+        $pager .= qq!<A HREF="!. $cgi->self_url. qq!">$page</A> !;
+      }
+    }
+    unless ( $offset + $maxrecords > $total ) {
+      $cgi->param('offset', $offset + $maxrecords);
+      $pager .= '<A HREF="'. $cgi->self_url.
+                '"><B><FONT SIZE="+1">Next</FONT></B></A> ';
+    }
+  }
+  #end pager
+  
+  if ( $cgi->param('showcancelledcustomers') eq '0' #see if it was set by me
+       || ( $conf->exists('hidecancelledcustomers')
+            && ! $cgi->param('showcancelledcustomers')
+          )
+     ) {
+    $cgi->param('showcancelledcustomers', 1);
+    $cgi->param('offset', 0);
+    print qq!( <a href="!. $cgi->self_url. qq!">show cancelled customers</a> )!;
+  } else {
+    $cgi->param('showcancelledcustomers', 0);
+    $cgi->param('offset', 0);
+    print qq!( <a href="!. $cgi->self_url. qq!">hide cancelled customers</a> )!;
+  }
+  if ( $cgi->param('referral_custnum') ) {
+    $cgi->param('referral_custnum') =~ /^(\d+)$/
+      or eidiot "Illegal referral_custnum\n";
+    my $referral_custnum = $1;
+    my $cust_main = qsearchs('cust_main', { custnum => $referral_custnum } );
+    print '<FORM METHOD=POST>'.
+          qq!<INPUT TYPE="hidden" NAME="referral_custnum" VALUE="$referral_custnum">!.
+          'referrals of <A HREF="'. popurl(2).
+          "view/cust_main.cgi?$referral_custnum\">$referral_custnum: ".
+          ( $cust_main->company
+            || $cust_main->last. ', '. $cust_main->first ).
+          '</A>';
+    print "\n",<<END;
+      <SCRIPT>
+      function changed(what) {
+        what.form.submit();
+      }
+      </SCRIPT>
+END
+    print ' <SELECT NAME="referral_depth" SIZE="1" onChange="changed(this)">';
+    my $max = 8; #config file
+    $cgi->param('referral_depth') =~ /^(\d*)$/ 
+      or eidiot "Illegal referral_depth";
+    my $referral_depth = $1;
+
+    foreach my $depth ( 1 .. $max ) {
+      print '<OPTION',
+            ' SELECTED'x($depth == $referral_depth),
+            ">$depth";
+    }
+    print "</SELECT> levels deep".
+          '<NOSCRIPT> <INPUT TYPE="submit" VALUE="change"></NOSCRIPT>'.
+          '</FORM>';
+  }
+
+  print "<BR><BR>". $pager. &table(). <<END;
+      <TR>
+        <TH></TH>
+        <TH>(bill) name</TH>
+        <TH>company</TH>
+END
+
+if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+  print <<END;
+      <TH>(service) name</TH>
+      <TH>company</TH>
+END
+}
+
+print <<END;
+        <TH>Packages</TH>
+        <TH COLSPAN=2>Services</TH>
+      </TR>
+END
+
+  my(%saw,$cust_main);
+  my $p = popurl(2);
+  foreach $cust_main (
+    sort $sortby grep(!$saw{$_->custnum}++, @cust_main)
+  ) {
+    my($custnum,$last,$first,$company)=(
+      $cust_main->custnum,
+      $cust_main->getfield('last'),
+      $cust_main->getfield('first'),
+      $cust_main->company,
+    );
+
+    my(@lol_cust_svc);
+    my($rowspan)=0;#scalar( @{$all_pkgs{$custnum}} );
+    foreach ( @{$all_pkgs{$custnum}} ) {
+      #my(@cust_svc) = qsearch( 'cust_svc', { 'pkgnum' => $_->pkgnum } );
+      my @cust_svc = $_->cust_svc;
+      push @lol_cust_svc, \@cust_svc;
+      $rowspan += scalar(@cust_svc) || 1;
+    }
+
+    #my($rowspan) = scalar(@{$all_pkgs{$custnum}});
+    my $view;
+    if ( defined $cgi->param('quickpay') && $cgi->param('quickpay') eq 'yes' ) {
+      $view = $p. 'edit/cust_pay.cgi?quickpay=yes;custnum='. $custnum;
+    } else {
+      $view = $p. 'view/cust_main.cgi?'. $custnum;
+    }
+    my $pcompany = $company
+      ? qq!<A HREF="$view"><FONT SIZE=-1>$company</FONT></A>!
+      : '<FONT SIZE=-1>&nbsp;</FONT>';
+    print <<END;
+    <TR>
+      <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$custnum</FONT></A></TD>
+      <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$last, $first</FONT></A></TD>
+      <TD ROWSPAN=$rowspan>$pcompany</TD>
+END
+    if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+      my($ship_last,$ship_first,$ship_company)=(
+        $cust_main->ship_last || $cust_main->getfield('last'),
+        $cust_main->ship_last ? $cust_main->ship_first : $cust_main->first,
+        $cust_main->ship_last ? $cust_main->ship_company : $cust_main->company,
+      );
+      my $pship_company = $ship_company
+        ? qq!<A HREF="$view"><FONT SIZE=-1>$ship_company</FONT></A>!
+        : '<FONT SIZE=-1>&nbsp;</FONT>';
+      print <<END;
+      <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$ship_last, $ship_first</FONT></A></TD>
+      <TD ROWSPAN=$rowspan>$pship_company</A></TD>
+END
+    }
+
+    my($n1)='';
+    foreach ( @{$all_pkgs{$custnum}} ) {
+      my $pkgnum = $_->pkgnum;
+#      my $part_pkg = qsearchs( 'part_pkg', { pkgpart => $_->pkgpart } );
+      my $part_pkg = $_->part_pkg;
+
+      my $pkg = $part_pkg->pkg;
+      my $comment = $part_pkg->comment;
+      my $pkgview = $p. 'view/cust_pkg.cgi?'. $pkgnum;
+      my @cust_svc = @{shift @lol_cust_svc};
+      #my(@cust_svc) = qsearch( 'cust_svc', { 'pkgnum' => $_->pkgnum } );
+      my $rowspan = scalar(@cust_svc) || 1;
+
+      print $n1, qq!<TD ROWSPAN=$rowspan><A HREF="$pkgview"><FONT SIZE=-1>$pkg - $comment</FONT></A></TD>!;
+      my($n2)='';
+      foreach my $cust_svc ( @cust_svc ) {
+         my($label, $value, $svcdb) = $cust_svc->label;
+         my($svcnum) = $cust_svc->svcnum;
+         my($sview) = $p.'view';
+         print $n2,qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$label</FONT></A></TD>!,
+               qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$value</FONT></A></TD>!;
+         $n2="</TR><TR>";
+      }
+      #print qq!</TR><TR>\n!;
+      $n1="</TR><TR>";
+    }
+    print "</TR>";
+  }
+  print "</TABLE>$pager</BODY></HTML>";
+
+}
+
+#undef $cache; #does this help?
+
+#
+
+sub last_sort {
+  lc($a->getfield('last')) cmp lc($b->getfield('last'))
+  || lc($a->first) cmp lc($b->first);
+}
+
+sub company_sort {
+  return -1 if $a->company && ! $b->company;
+  return 1 if ! $a->company && $b->company;
+  lc($a->company) cmp lc($b->company)
+  || lc($a->getfield('last')) cmp lc($b->getfield('last'))
+  || lc($a->first) cmp lc($b->first);;
+}
+
+sub custnum_sort {
+  $a->getfield('custnum') <=> $b->getfield('custnum');
+}
+
+sub custnumsearch {
+
+  my $custnum = $cgi->param('custnum_text');
+  $custnum =~ s/\D//g;
+  $custnum =~ /^(\d{1,23})$/ or eidiot "Illegal customer number\n";
+  $custnum = $1;
+  
+  [ qsearchs('cust_main', { 'custnum' => $custnum } ) ];
+}
+
+sub cardsearch {
+
+  my($card)=$cgi->param('card');
+  $card =~ s/\D//g;
+  $card =~ /^(\d{13,16})$/ or eidiot "Illegal card number\n";
+  my($payinfo)=$1;
+
+  [ qsearch('cust_main',{'payinfo'=>$payinfo, 'payby'=>'CARD'}),
+    qsearch('cust_main',{'payinfo'=>$payinfo, 'payby'=>'DCRD'})
+  ];
+}
+
+sub referralsearch {
+  $cgi->param('referral_custnum') =~ /^(\d+)$/
+    or eidiot "Illegal referral_custnum";
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $1 } )
+    or eidiot "Customer $1 not found";
+  my $depth;
+  if ( $cgi->param('referral_depth') ) {
+    $cgi->param('referral_depth') =~ /^(\d+)$/
+      or eidiot "Illegal referral_depth";
+    $depth = $1;
+  } else {
+    $depth = 1;
+  }
+  [ $cust_main->referral_cust_main($depth) ];
+}
+
+sub lastsearch {
+  my(%last_type);
+  my @cust_main;
+  foreach ( $cgi->param('last_type') ) {
+    $last_type{$_}++;
+  }
+
+  $cgi->param('last_text') =~ /^([\w \,\.\-\']*)$/
+    or eidiot "Illegal last name";
+  my($last)=$1;
+
+  if ( $last_type{'Exact'} || $last_type{'Fuzzy'} ) {
+    push @cust_main, qsearch( 'cust_main',
+                              { 'last' => { 'op'    => 'ILIKE',
+                                            'value' => $last    } } );
+
+    push @cust_main, qsearch( 'cust_main',
+                              { 'ship_last' => { 'op'    => 'ILIKE',
+                                                 'value' => $last    } } )
+      if defined dbdef->table('cust_main')->column('ship_last');
+  }
+
+  if ( $last_type{'Substring'} || $last_type{'All'} ) {
+
+    push @cust_main, qsearch( 'cust_main',
+                              { 'last' => { 'op'    => 'ILIKE',
+                                            'value' => "%$last%" } } );
+
+    push @cust_main, qsearch( 'cust_main',
+                              { 'ship_last' => { 'op'    => 'ILIKE',
+                                                 'value' => "%$last%" } } )
+      if defined dbdef->table('cust_main')->column('ship_last');
+
+  }
+
+  if ( $last_type{'Fuzzy'} || $last_type{'All'} ) {
+
+    &FS::cust_main::check_and_rebuild_fuzzyfiles;
+    my $all_last = &FS::cust_main::all_last;
+
+    my %last;
+    if ( $last_type{'Fuzzy'} || $last_type{'All'} ) { 
+      foreach ( amatch($last, [ qw(i) ], @$all_last) ) {
+        $last{$_}++; 
+      }
+    }
+
+    #if ($last_type{'Sound-alike'}) {
+    #}
+
+    foreach ( keys %last ) {
+      push @cust_main, qsearch('cust_main',{'last'=>$_});
+      push @cust_main, qsearch('cust_main',{'ship_last'=>$_})
+        if defined dbdef->table('cust_main')->column('ship_last');
+    }
+
+  }
+
+  \@cust_main;
+}
+
+sub companysearch {
+
+  my(%company_type);
+  my @cust_main;
+  foreach ( $cgi->param('company_type') ) {
+    $company_type{$_}++ 
+  };
+
+  $cgi->param('company_text') =~
+    /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
+      or eidiot "Illegal company";
+  my $company = $1;
+
+  if ( $company_type{'Exact'} || $company_type{'Fuzzy'} ) {
+    push @cust_main, qsearch( 'cust_main',
+                              { 'company' => { 'op'    => 'ILIKE',
+                                               'value' => $company } } );
+
+    push @cust_main, qsearch( 'cust_main',
+                              { 'ship_company' => { 'op'    => 'ILIKE',
+                                                    'value' => $company } } )
+      if defined dbdef->table('cust_main')->column('ship_last');
+  }
+
+  if ( $company_type{'Substring'} || $company_type{'All'} ) {
+
+    push @cust_main, qsearch( 'cust_main',
+                              { 'company' => { 'op'    => 'ILIKE',
+                                               'value' => "%$company%" } } );
+
+    push @cust_main, qsearch( 'cust_main',
+                              { 'ship_company' => { 'op'    => 'ILIKE',
+                                                    'value' => "%$company%" } })
+      if defined dbdef->table('cust_main')->column('ship_last');
+
+  }
+
+  if ( $company_type{'Fuzzy'} || $company_type{'All'} ) {
+
+    &FS::cust_main::check_and_rebuild_fuzzyfiles;
+    my $all_company = &FS::cust_main::all_company;
+
+    my %company;
+    if ( $company_type{'Fuzzy'} || $company_type{'All'} ) { 
+      foreach ( amatch($company, [ qw(i) ], @$all_company ) ) {
+        $company{$_}++;
+      }
+    }
+
+    #if ($company_type{'Sound-alike'}) {
+    #}
+
+    foreach ( keys %company ) {
+      push @cust_main, qsearch('cust_main',{'company'=>$_});
+      push @cust_main, qsearch('cust_main',{'ship_company'=>$_})
+        if defined dbdef->table('cust_main')->column('ship_last');
+    }
+
+  }
+
+  \@cust_main;
+}
+
+sub address2search {
+  my @cust_main;
+
+  $cgi->param('address2_text') =~
+    /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
+      or eidiot "Illegal address2";
+  my $address2 = $1;
+
+  push @cust_main, qsearch( 'cust_main',
+                            { 'address2' => { 'op'    => 'ILIKE',
+                                              'value' => $address2 } } );
+  push @cust_main, qsearch( 'cust_main',
+                            { 'address2' => { 'op'    => 'ILIKE',
+                                              'value' => $address2 } } )
+    if defined dbdef->table('cust_main')->column('ship_last');
+
+  \@cust_main;
+}
+
+sub phonesearch {
+  my @cust_main;
+
+  my $phone = $cgi->param('phone_text');
+
+  #(no longer really) false laziness with Record::ut_phonen
+  #only works with US/CA numbers...
+  $phone =~ s/\D//g;
+  if ( $phone =~ /^(\d{3})(\d{3})(\d{4})(\d*)$/ ) {
+    $phone = "$1-$2-$3";
+    $phone .= " x$4" if $4;
+  } elsif ( $phone =~ /^(\d{3})(\d{4})$/ ) {
+    $phone = "$1-$2";
+  } elsif ( $phone =~ /^(\d{3,4})$/ ) {
+    $phone = $1;
+  } else {
+    eidiot gettext('illegal_phone'). ": $phone";
+  }
+
+  my @fields = qw(daytime night fax);
+  push @fields, qw(ship_daytime ship_night ship_fax)
+    if defined dbdef->table('cust_main')->column('ship_last');
+
+  for my $field ( @fields ) {
+    push @cust_main, qsearch ( 'cust_main', 
+                               { $field => { 'op'    => 'LIKE',
+                                             'value' => "%$phone%" } } );
+  }
+
+  \@cust_main;
+}
+
+%>
diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html
new file mode 100755 (executable)
index 0000000..5a066e4
--- /dev/null
@@ -0,0 +1,42 @@
+<HTML>
+  <HEAD>
+    <TITLE>Customer Search</TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <FONT SIZE=7>
+      Customer Search
+    </FONT>
+    <BR><BR>
+    <FORM ACTION="cust_main.cgi" METHOD="post">
+      <INPUT TYPE="checkbox" NAME="last_on" CHECKED> Search for <B>last name</B>: 
+      <INPUT TYPE="text" NAME="last_text">
+      using search method: <SELECT NAME="last_type">
+        <OPTION SELECTED>All
+        <OPTION>Fuzzy
+        <OPTION>Substring
+        <OPTION>Exact
+      </SELECT>
+
+      <P><INPUT TYPE="checkbox" NAME="company_on" CHECKED> Search for <B>company</B>: 
+      <INPUT TYPE="text" NAME="company_text">
+      using search methods: <SELECT NAME="company_type">
+        <OPTION SELECTED>All
+        <OPTION>Fuzzy
+        <OPTION>Substring
+        <OPTION>Exact
+      </SELECT>
+
+      <P><INPUT TYPE="submit" VALUE="Search"> Note: Fuzzy searching can take a while.  Please be patient.
+
+    </FORM>
+
+  <HR>Explanation of search methods:
+  <UL>
+    <LI><B>All</B> - Try all search methods.
+    <LI><B>Fuzzy</B> - Searches for matches that are close to your text.
+    <LI><B>Substring</B> - Searches for matches that contain your text.
+    <LI><B>Exact</B> - Finds exact matches only, but much faster than the other search methods.
+  </UL>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_pay.cgi b/httemplate/search/cust_pay.cgi
new file mode 100755 (executable)
index 0000000..7a98370
--- /dev/null
@@ -0,0 +1,151 @@
+<%
+
+my $sortby;
+my @cust_pay;
+if ( $cgi->param('magic') && $cgi->param('magic') eq '_date' ) {
+
+  my %search;
+  if ( $cgi->param('payby') ) {
+    $cgi->param('payby') =~ /^(CARD|CHEK|BILL)$/
+      or die "illegal payby ". $cgi->param('payby');
+    $search{'payby'} = $1;
+  }
+
+  #false laziness with cust_pkg.cgi
+  my $range = '';
+  if ( $cgi->param('beginning')
+       && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+    my $beginning = str2time($1);
+    $range = " WHERE _date >= $beginning ";
+  }
+  if ( $cgi->param('ending')
+            && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+    my $ending = str2time($1) + 86400;
+    $range .= ( $range ? ' AND ' : ' WHERE ' ). " _date <= $ending ";
+  }
+  $range =~ s/^\s*WHERE/ AND/ if scalar(keys %search) ;
+
+  @cust_pay = qsearch('cust_pay', \%search, '', $range );
+
+  $sortby = \*date_sort;
+
+} else {
+
+  $cgi->param('payinfo') =~ /^\s*(\d+)\s*$/ or die "illegal payinfo";
+  my $payinfo = $1;
+
+  $cgi->param('payby') =~ /^(\w+)$/ or die "illegal payby";
+  my $payby = $1;
+
+  @cust_pay = qsearch('cust_pay', { 'payinfo' => $payinfo,
+                                     'payby'   => $payby    } );
+  $sortby = \*date_sort;
+
+}
+
+if (0) {
+#if ( scalar(@cust_pay) == 1 ) {
+#  my $invnum = $cust_bill[0]->invnum;
+#  print $cgi->redirect(popurl(2). "view/cust_bill.cgi?$invnum");  #redirect
+} elsif ( scalar(@cust_pay) == 0 ) {
+%>
+<!-- mason kludge -->
+<%
+  idiot("Payment not found.");
+  #exit;
+} else {
+  my $total = scalar(@cust_pay);
+  my $s = $total > 1 ? 's' : '';
+%>
+<!-- mason kludge -->
+<%
+  print header("Payment Search Results", menubar(
+          'Main Menu', popurl(2)
+        )), "$total matching payment$s found<BR>", &table(), <<END;
+      <TR>
+        <TH></TH>
+        <TH>Amount</TH>
+        <TH>Date</TH>
+        <TH>Contact name</TH>
+        <TH>Company</TH>
+      </TR>
+END
+
+  my(%saw, $cust_pay);
+  foreach my $cust_pay (
+    sort $sortby grep(!$saw{$_->paynum}++, @cust_pay)
+  ) {
+    my($paynum, $custnum, $payby, $payinfo, $amount, $date ) = (
+      $cust_pay->paynum,
+      $cust_pay->custnum,
+      $cust_pay->payby,
+      $cust_pay->payinfo,
+      sprintf("%.2f", $cust_pay->paid),
+      $cust_pay->_date,
+    );
+    my $pdate = time2str("%b&nbsp;%d&nbsp;%Y", $date);
+
+    my $rowspan = 1;
+
+    my $view = popurl(2). "view/cust_main.cgi?". $custnum. 
+               "#". $payby. $payinfo;
+
+    my $payment_info;
+    if ( $payby eq 'CARD' ) {
+      $payment_info = 'Card&nbsp;#'. 'x'x(length($payinfo)-4).
+                      substr($payinfo,(length($payinfo)-4));
+    } elsif ( $payby eq 'CHEK' ) {
+      $payment_info = "E-check&nbsp;acct#$payinfo";
+    } elsif ( $payby eq 'BILL' ) {
+      $payment_info = "Check&nbsp;#$payinfo";
+    } else {
+      $payment_info = "$payby&nbsp;$payinfo";
+    }
+
+    print <<END;
+      <TR>
+        <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$payment_info</FONT></A></TD>
+        <TD ROWSPAN=$rowspan ALIGN="right"><A HREF="$view"><FONT SIZE=-1>\$$amount</FONT></A></TD>
+        <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$pdate</FONT></A></TD>
+END
+    my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
+    if ( $cust_main ) {
+      #my $cview = popurl(2). "view/cust_main.cgi?". $cust_main->custnum;
+      my ( $name, $company ) = (
+        $cust_main->last. ', '. $cust_main->first,
+        $cust_main->company,
+      );
+      print <<END;
+        <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$name</FONT></A></TD>
+        <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$company</FONT></A></TD>
+END
+    } else {
+      print <<END
+        <TD ROWSPAN=$rowspan COLSPAN=2>WARNING: couldn't find cust_main.custnum $custnum (cust_pay.paynum $paynum)</TD>
+END
+    }
+
+    print "</TR>";
+  }
+  print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+}
+
+#
+
+#sub invnum_sort {
+#  $a->invnum <=> $b->invnum;
+#}
+#
+#sub custnum_sort {
+#  $a->custnum <=> $b->custnum || $a->invnum <=> $b->invnum;
+#}
+
+sub date_sort {
+  $a->_date <=> $b->_date || $a->invnum <=> $b->invnum;
+}
+%>
diff --git a/httemplate/search/cust_pay.html b/httemplate/search/cust_pay.html
new file mode 100755 (executable)
index 0000000..3848d66
--- /dev/null
@@ -0,0 +1,18 @@
+<HTML>
+  <HEAD>
+    <TITLE>Check # Search</TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <FONT SIZE=7>
+      Check # Search
+    </FONT>
+    <BR><BR>
+    <FORM ACTION="cust_pay.cgi" METHOD="post">
+      Search for <B>check #</B>:
+      <INPUT TYPE="text" NAME="payinfo">
+      <INPUT TYPE="hidden" NAME="payby" VALUE="BILL">
+      <BR><BR><INPUT TYPE="submit" VALUE="Search">
+    </FORM>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/cust_pkg.cgi b/httemplate/search/cust_pkg.cgi
new file mode 100755 (executable)
index 0000000..8b2fd0c
--- /dev/null
@@ -0,0 +1,350 @@
+<%
+
+my $conf = new FS::Conf;
+my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+my %part_pkg = map { $_->pkgpart => $_ } qsearch('part_pkg', {});
+
+my $limit = '';
+$limit .= "LIMIT $maxrecords" if $maxrecords;
+
+my $offset = $cgi->param('offset') || 0;
+$limit .= " OFFSET $offset" if $offset;
+
+my $total;
+
+my($query) = $cgi->keywords;
+my $sortby;
+my @cust_pkg;
+
+if ( $cgi->param('magic') && $cgi->param('magic') eq 'bill' ) {
+  $sortby=\*bill_sort;
+
+  #false laziness with cust_pay.cgi
+  my $range = '';
+  if ( $cgi->param('beginning')
+       && $cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+    my $beginning = str2time($1);
+    $range = " WHERE bill >= $beginning ";
+  }
+  if ( $cgi->param('ending')
+            && $cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/ ) {
+    my $ending = str2time($1) + 86400;
+    $range .= ( $range ? ' AND ' : ' WHERE ' ). " bill <= $ending ";
+  }
+
+  #false laziness with below
+  my $statement = "SELECT COUNT(*) FROM cust_pkg $range";
+  warn $statement;
+  my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
+  $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+  
+  $total = $sth->fetchrow_arrayref->[0];
+  
+  @cust_pkg = qsearch('cust_pkg',{}, '', " $range ORDER BY bill $limit" );
+
+} else {
+
+  my $qual = '';
+  if ( $cgi->param('magic') && $cgi->param('magic') eq 'active' ) {
+
+    $qual = 'WHERE ( susp IS NULL OR susp = 0 )'.
+            ' AND ( cancel IS NULL OR cancel = 0)';
+
+    $sortby = \*pkgnum_sort;
+
+    if ( $cgi->param('pkgpart') =~ /^(\d+)$/ ) {
+      $qual .= " AND pkgpart = $1";
+    }
+
+  } elsif ( $query eq 'pkgnum' ) {
+
+    $sortby=\*pkgnum_sort;
+
+  } elsif ( $query eq 'SUSP_pkgnum' ) {
+
+    $sortby=\*pkgnum_sort;
+
+    $qual = 'WHERE susp IS NOT NULL AND susp != 0';
+
+  } elsif ( $query eq 'APKG_pkgnum' ) {
+  
+    $sortby=\*pkgnum_sort;
+  
+    #@cust_pkg=();
+    ##perhaps this should go in cust_pkg as a qsearch-like constructor?
+    #my($cust_pkg);
+    #foreach $cust_pkg (
+    #  qsearch('cust_pkg',{}, '', "ORDER BY pkgnum $limit" )
+    #) {
+    #  my($flag)=0;
+    #  my($pkg_svc);
+    #  PKG_SVC: 
+    #  foreach $pkg_svc (qsearch('pkg_svc',{ 'pkgpart' => $cust_pkg->pkgpart })) {
+    #    if ( $pkg_svc->quantity 
+    #         > scalar(qsearch('cust_svc',{
+    #             'pkgnum' => $cust_pkg->pkgnum,
+    #             'svcpart' => $pkg_svc->svcpart,
+    #           }))
+    #       )
+    #    {
+    #      $flag=1;
+    #      last PKG_SVC;
+    #    }
+    #  }
+    #  push @cust_pkg, $cust_pkg if $flag;
+    #}
+
+    if ( driver_name eq 'mysql' ) {
+      #$query = "DROP TABLE temp1_$$,temp2_$$;";
+      #my $sth = dbh->prepare($query);
+      #$sth->execute;
+
+      $query = "CREATE TEMPORARY TABLE temp1_$$ TYPE=MYISAM
+                  SELECT cust_svc.pkgnum,cust_svc.svcpart,COUNT(*) as count
+                    FROM cust_pkg,cust_svc,pkg_svc
+                      WHERE cust_pkg.pkgnum = cust_svc.pkgnum
+                      AND cust_svc.svcpart = pkg_svc.svcpart
+                      AND cust_pkg.pkgpart = pkg_svc.pkgpart
+                      GROUP BY cust_svc.pkgnum,cust_svc.svcpart";
+      my $sth = dbh->prepare($query) or die dbh->errstr. " preparing $query";
+         
+      $sth->execute or die "Error executing \"$query\": ". $sth->errstr;
+  
+      $query = "CREATE TEMPORARY TABLE temp2_$$ TYPE=MYISAM
+                  SELECT cust_pkg.pkgnum FROM cust_pkg
+                    LEFT JOIN pkg_svc ON (cust_pkg.pkgpart=pkg_svc.pkgpart)
+                    LEFT JOIN temp1_$$ ON (cust_pkg.pkgnum = temp1_$$.pkgnum
+                                           AND pkg_svc.svcpart=temp1_$$.svcpart)
+                    WHERE ( pkg_svc.quantity > temp1_$$.count
+                            OR temp1_$$.pkgnum IS NULL )
+                          AND pkg_svc.quantity != 0;";
+      $sth = dbh->prepare($query) or die dbh->errstr. " preparing $query";   
+      $sth->execute or die "Error executing \"$query\": ". $sth->errstr;
+      $qual = " LEFT JOIN temp2_$$ ON cust_pkg.pkgnum = temp2_$$.pkgnum
+                  WHERE temp2_$$.pkgnum IS NOT NULL";
+
+    } else {
+
+     $qual = "
+       WHERE 0 <
+         ( SELECT count(*) FROM pkg_svc
+             WHERE pkg_svc.pkgpart = cust_pkg.pkgpart
+               AND pkg_svc.quantity > ( SELECT count(*) FROM cust_svc
+                                        WHERE cust_svc.pkgnum = cust_pkg.pkgnum
+                                          AND cust_svc.svcpart = pkg_svc.svcpart
+                                      )
+         )
+     ";
+
+    }
+    
+  } else {
+    die "Empty or unknown QUERY_STRING!";
+  }
+  
+  my $statement = "SELECT COUNT(*) FROM cust_pkg $qual";
+  my $sth = dbh->prepare($statement) or die dbh->errstr." preparing $statement";
+  $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+  
+  $total = $sth->fetchrow_arrayref->[0];
+
+  my $tblname = driver_name eq 'mysql' ? 'cust_pkg.' : '';
+  @cust_pkg =
+    qsearch('cust_pkg',{}, '', "$qual ORDER BY ${tblname}pkgnum $limit" );
+
+  if ( driver_name eq 'mysql' ) {
+    $query = "DROP TABLE temp1_$$,temp2_$$;";
+    my $sth = dbh->prepare($query) or die dbh->errstr. " doing $query";
+    $sth->execute; # or die "Error executing \"$query\": ". $sth->errstr;
+  }
+  
+}
+
+if ( scalar(@cust_pkg) == 1 ) {
+  my($pkgnum)=$cust_pkg[0]->pkgnum;
+  print $cgi->redirect(popurl(2). "view/cust_pkg.cgi?$pkgnum");
+  #exit;
+} elsif ( scalar(@cust_pkg) == 0 ) { #error
+%>
+<!-- mason kludge -->
+<%
+  eidiot("No packages found");
+} else {
+%>
+<!-- mason kludge -->
+<%
+  $total ||= scalar(@cust_pkg);
+
+  #begin pager
+  my $pager = '';
+  if ( $total != scalar(@cust_pkg) && $maxrecords ) {
+    unless ( $offset == 0 ) {
+      $cgi->param('offset', $offset - $maxrecords);
+      $pager .= '<A HREF="'. $cgi->self_url.
+                '"><B><FONT SIZE="+1">Previous</FONT></B></A> ';
+    }
+    my $poff;
+    my $page;
+    for ( $poff = 0; $poff < $total; $poff += $maxrecords ) {
+      $page++;
+      if ( $offset == $poff ) {
+        $pager .= qq!<FONT SIZE="+2">$page</FONT> !;
+      } else {
+        $cgi->param('offset', $poff);
+        $pager .= qq!<A HREF="!. $cgi->self_url. qq!">$page</A> !;
+      }
+    }
+    unless ( $offset + $maxrecords > $total ) {
+      $cgi->param('offset', $offset + $maxrecords);
+      $pager .= '<A HREF="'. $cgi->self_url.
+                '"><B><FONT SIZE="+1">Next</FONT></B></A> ';
+    }
+  }
+  #end pager
+  
+  print header('Package Search Results',''),
+        "$total matching packages found<BR><BR>$pager", &table(), <<END;
+      <TR>
+        <TH>Package</TH>
+        <TH><FONT SIZE=-1>Setup</FONT></TH>
+END
+
+  print '<TH><FONT SIZE=-1>Last<BR>bill</FONT></TH>'
+    if defined dbdef->table('cust_pkg')->column('last_bill');
+
+  print <<END;
+        <TH><FONT SIZE=-1>Next<BR>bill</FONT></TH>
+        <TH><FONT SIZE=-1>Susp.</FONT></TH>
+        <TH><FONT SIZE=-1>Expire</FONT></TH>
+        <TH><FONT SIZE=-1>Cancel</FONT></TH>
+        <TH><FONT SIZE=-1>Cust#</FONT></TH>
+        <TH>(bill) name</TH>
+        <TH>company</TH>
+END
+
+  print '<TH>(service) name</TH><TH>company</TH>'
+    if defined dbdef->table('cust_main')->column('ship_last');
+
+  print '<TH COLSPAN=2>Services</TH></TR>';
+
+  my $n1 = '<TR>';
+  my(%saw,$cust_pkg);
+  foreach $cust_pkg (
+    sort $sortby grep(!$saw{$_->pkgnum}++, @cust_pkg)
+  ) {
+    my($cust_main)=qsearchs('cust_main',{'custnum'=>$cust_pkg->custnum});
+    my($pkgnum, $setup, $bill, $susp, $expire, $cancel,
+       $custnum, $last, $first, $company ) = (
+      $cust_pkg->pkgnum,
+      $cust_pkg->getfield('setup')
+        ? time2str("%D", $cust_pkg->getfield('setup') )
+        : '',
+      $cust_pkg->getfield('bill')
+        ? time2str("%D", $cust_pkg->getfield('bill') )
+        : '',
+      $cust_pkg->getfield('susp')
+        ? time2str("%D", $cust_pkg->getfield('susp') )
+        : '',
+      $cust_pkg->getfield('expire')
+        ? time2str("%D", $cust_pkg->getfield('expire') )
+        : '',
+      $cust_pkg->getfield('cancel')
+        ? time2str("%D", $cust_pkg->getfield('cancel') )
+        : '',
+      $cust_pkg->custnum,
+      $cust_main ? $cust_main->last : '',
+      $cust_main ? $cust_main->first : '',
+      $cust_main ? $cust_main->company : '',
+    );
+
+    my $last_bill = $cust_pkg->getfield('last_bill')
+                      ? time2str("%D", $cust_pkg->getfield('last_bill') )
+                      : ''
+      if defined dbdef->table('cust_pkg')->column('last_bill');
+
+    my($ship_last, $ship_first, $ship_company);
+    if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+      ($ship_last, $ship_first, $ship_company) = (
+        $cust_main
+          ? ( $cust_main->ship_last || $cust_main->getfield('last') )
+          : '',
+        $cust_main 
+          ? ( $cust_main->ship_last
+              ? $cust_main->ship_first
+              : $cust_main->first )
+          : '',
+        $cust_main 
+          ? ( $cust_main->ship_last
+              ? $cust_main->ship_company
+              : $cust_main->company )
+          : '',
+      );
+    }
+    my $pkg = $part_pkg{$cust_pkg->pkgpart}->pkg;
+    #$pkg .= ' - '. $part_pkg{$cust_pkg->pkgpart}->comment;
+    my @cust_svc = qsearch( 'cust_svc', { 'pkgnum' => $pkgnum } );
+    my $rowspan = scalar(@cust_svc) || 1;
+    my $p = popurl(2);
+    print $n1, <<END;
+      <TD ROWSPAN=$rowspan><A HREF="${p}view/cust_pkg.cgi?$pkgnum"><FONT SIZE=-1>$pkgnum - $pkg</FONT></A></TD>
+      <TD ROWSPAN=$rowspan>$setup</TD>
+END
+
+    print "<TD ROWSPAN=$rowspan>$last_bill</TD>"
+      if defined dbdef->table('cust_pkg')->column('last_bill');
+
+    print <<END;
+      <TD ROWSPAN=$rowspan>$bill</TD>
+      <TD ROWSPAN=$rowspan>$susp</TD>
+      <TD ROWSPAN=$rowspan>$expire</TD>
+      <TD ROWSPAN=$rowspan>$cancel</TD>
+END
+    if ( $cust_main ) {
+      print <<END;
+      <TD ROWSPAN=$rowspan><FONT SIZE=-1><A HREF="${p}view/cust_main.cgi?$custnum">$custnum</A></FONT></TD>
+      <TD ROWSPAN=$rowspan><FONT SIZE=-1><A HREF="${p}view/cust_main.cgi?$custnum">$last, $first</A></FONT></TD>
+      <TD ROWSPAN=$rowspan><FONT SIZE=-1><A HREF="${p}view/cust_main.cgi?$custnum">$company</A></FONT></TD>
+END
+      if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+        print <<END;
+      <TD ROWSPAN=$rowspan><FONT SIZE=-1><A HREF="${p}view/cust_main.cgi?$custnum">$ship_last, $ship_first</A></FONT></TD>
+      <TD ROWSPAN=$rowspan><FONT SIZE=-1><A HREF="${p}view/cust_main.cgi?$custnum">$ship_company</A></FONT></TD>
+END
+      }
+    } else {
+      my $colspan = defined dbdef->table('cust_main')->column('ship_last')
+                    ? 5 : 3;
+      print <<END;
+      <TD ROWSPAN=$rowspan COLSPAN=$colspan>WARNING: couldn't find cust_main.custnum $custnum (cust_pkg.pkgnum $pkgnum)</TD>
+END
+    }
+
+    my $n2 = '';
+    foreach my $cust_svc ( @cust_svc ) {
+      my($label, $value, $svcdb) = $cust_svc->label;
+      my $svcnum = $cust_svc->svcnum;
+      my $sview = $p. "view";
+      print $n2,qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$label</FONT></A></TD>!,
+            qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$value</FONT></A></TD>!;
+      $n2="</TR><TR>";
+    }
+
+    $n1 = "</TR><TR>";
+
+  }
+    print '</TR>';
+  print "</TABLE>$pager</BODY></HTML>";
+
+}
+
+sub pkgnum_sort {
+  $a->getfield('pkgnum') <=> $b->getfield('pkgnum');
+}
+
+sub bill_sort {
+  $a->getfield('bill') <=> $b->getfield('bill');
+}
+
+%>
diff --git a/httemplate/search/cust_pkg.html b/httemplate/search/cust_pkg.html
new file mode 100755 (executable)
index 0000000..bb0a540
--- /dev/null
@@ -0,0 +1,24 @@
+<HTML>
+  <HEAD>
+    <TITLE>Packages</TITLE>
+  </HEAD>
+  <BODY>
+    <CENTER>
+      <H1>Packages</H1>
+    </CENTER>
+    <HR>
+    <FORM ACTION="cust_pkg.cgi" METHOD="post">
+    <INPUT TYPE="hidden" NAME="magic" VALUE="bill">
+      Return <B>packages</B> with next bill date: 
+      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
+      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
+
+      <P><INPUT TYPE="submit" VALUE="Get Report">
+
+    </FORM>
+
+  <HR>
+
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/report_cc.cgi b/httemplate/search/report_cc.cgi
new file mode 100755 (executable)
index 0000000..c2ab726
--- /dev/null
@@ -0,0 +1,25 @@
+<!-- mason kludge -->
+<%
+
+my $user = getotaker;
+
+$cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/;
+my $beginning = $1;
+
+$cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/;
+my $ending = $1;
+
+print header('Credit Card Recipt Report Results');
+
+open (REPORT, "freeside-cc-receipts-report -v -s $beginning -f $ending $user |");
+
+print '<PRE>';
+while(<REPORT>) {
+  print $_;
+}
+print '</PRE>';
+
+print '</BODY></HTML>';
+
+%>
+
diff --git a/httemplate/search/report_cc.html b/httemplate/search/report_cc.html
new file mode 100755 (executable)
index 0000000..8653dcc
--- /dev/null
@@ -0,0 +1,23 @@
+<HTML>
+  <HEAD>
+    <TITLE>Credit Card Receipt Report Criteria</TITLE>
+  </HEAD>
+  <BODY>
+    <CENTER>
+      <H1>Credit Card Receipt Report Criteria</H1>
+    </CENTER>
+    <HR>
+    <FORM ACTION="report_cc.cgi" METHOD="post">
+      Return <B>credit card receipt report</B> for period: 
+      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
+      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
+
+      <P><INPUT TYPE="submit" VALUE="Get Report">
+
+    </FORM>
+
+  <HR>
+
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/report_credit.cgi b/httemplate/search/report_credit.cgi
new file mode 100755 (executable)
index 0000000..2adafc0
--- /dev/null
@@ -0,0 +1,25 @@
+<!-- mason kludge -->
+<%
+
+my $user = getotaker;
+
+$cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/;
+my $beginning = $1;
+
+$cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/;
+my $ending = $1;
+
+print header('In House Credit Report Results');
+
+open (REPORT, "freeside-credit-report -v -s $beginning -f $ending $user |");
+
+print '<PRE>';
+while(<REPORT>) {
+  print $_;
+}
+print '</PRE>';
+
+print '</BODY></HTML>';
+
+%>
+
diff --git a/httemplate/search/report_credit.html b/httemplate/search/report_credit.html
new file mode 100755 (executable)
index 0000000..df9b958
--- /dev/null
@@ -0,0 +1,23 @@
+<HTML>
+  <HEAD>
+    <TITLE>In House Credit Report Criteria</TITLE>
+  </HEAD>
+  <BODY>
+    <CENTER>
+      <H1>In House Credit Report Criteria</H1>
+    </CENTER>
+    <HR>
+    <FORM ACTION="report_credit.cgi" METHOD="post">
+      Return <B>in house credit report</B> for period: 
+      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
+      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
+
+      <P><INPUT TYPE="submit" VALUE="Get Report">
+
+    </FORM>
+
+  <HR>
+
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/report_cust_pay.html b/httemplate/search/report_cust_pay.html
new file mode 100644 (file)
index 0000000..93053e1
--- /dev/null
@@ -0,0 +1,24 @@
+<HTML>
+  <HEAD>
+    <TITLE>Payment report criteria</TITLE>
+  </HEAD>
+  <BODY>
+    <CENTER>
+      <H1>Payment report criteria</H1>
+    </CENTER>
+    <HR>
+    <FORM ACTION="cust_pay.cgi" METHOD="post">
+    <INPUT TYPE="hidden" NAME="magic" VALUE="_date">
+      Return <SELECT NAME="payby">
+        <OPTION VALUE="">all</OPTION>
+        <OPTION VALUE="CARD">credit card</OPTION>
+        <OPTION VALUE="CHEK">electronic check (ACH)</OPTION>
+        <OPTION VALUE="BILL">check/cash</OPTION>
+      </SELECT> payments for period<BR>
+      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
+      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
+      <P><INPUT TYPE="submit" VALUE="Get Report">
+    </FORM>
+  <HR>
+  </BODY>
+</HTML>
diff --git a/httemplate/search/report_receivables.cgi b/httemplate/search/report_receivables.cgi
new file mode 100755 (executable)
index 0000000..fdd3779
--- /dev/null
@@ -0,0 +1,19 @@
+<!-- mason kludge -->
+<%
+
+my $user = getotaker;
+
+print header('Current Receivables Report Results');
+
+open (REPORT, "freeside-receivables-report -v $user |");
+
+print '<PRE>';
+while(<REPORT>) {
+  print $_;
+}
+print '</PRE>';
+
+print '</BODY></HTML>';
+
+%>
+
diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi
new file mode 100755 (executable)
index 0000000..ac76fad
--- /dev/null
@@ -0,0 +1,25 @@
+<!-- mason kludge -->
+<%
+
+my $user = getotaker;
+
+$cgi->param('beginning') =~ /^([ 0-9\-\/]{0,10})$/;
+my $beginning = $1;
+
+$cgi->param('ending') =~ /^([ 0-9\-\/]{0,10})$/;
+my $ending = $1;
+
+print header('Tax Report Results');
+
+open (REPORT, "freeside-tax-report -v -s $beginning -f $ending $user |");
+
+print '<PRE>';
+while(<REPORT>) {
+  print $_;
+}
+print '</PRE>';
+
+print '</BODY></HTML>';
+
+%>
+
diff --git a/httemplate/search/report_tax.html b/httemplate/search/report_tax.html
new file mode 100755 (executable)
index 0000000..7bf681b
--- /dev/null
@@ -0,0 +1,23 @@
+<HTML>
+  <HEAD>
+    <TITLE>Tax Report Criteria</TITLE>
+  </HEAD>
+  <BODY>
+    <CENTER>
+      <H1>Tax Report Criteria</H1>
+    </CENTER>
+    <HR>
+    <FORM ACTION="report_tax.cgi" METHOD="post">
+      Return <B>tax report</B> for period: 
+      from <INPUT TYPE="text" NAME="beginning"> <i>m/d/y</i>
+      to <INPUT TYPE="text" NAME="ending"> <i>m/d/y</i>
+
+      <P><INPUT TYPE="submit" VALUE="Get Report">
+
+    </FORM>
+
+  <HR>
+
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/sql.cgi b/httemplate/search/sql.cgi
new file mode 100755 (executable)
index 0000000..b83ef03
--- /dev/null
@@ -0,0 +1,76 @@
+<%
+
+my $conf = new FS::Conf;
+my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+my $limit = '';
+$limit .= "LIMIT $maxrecords" if $maxrecords;
+
+my $offset = $cgi->param('offset') || 0;
+$limit .= " OFFSET $offset" if $offset;
+
+my $total;
+
+my $sql = $cgi->param('sql');
+$sql =~ s/^\s*SELECT//i;
+
+my $count_sql = $sql;
+$count_sql =~ s/^(.*)\s+FROM\s/COUNT(*) FROM /i;
+
+my $sth = dbh->prepare("SELECT $count_sql")
+  or eidiot dbh->errstr. " doing $count_sql\n";
+$sth->execute or eidiot "Error executing \"$count_sql\": ". $sth->errstr;
+
+$total = $sth->fetchrow_arrayref->[0];
+
+my $sth = dbh->prepare("SELECT $sql $limit")
+  or eidiot dbh->errstr. " doing $sql\n";
+$sth->execute or eidiot "Error executing \"$sql\": ". $sth->errstr;
+my $rows = $sth->fetchall_arrayref;
+
+%>
+<!-- mason kludge -->
+<%
+
+  #begin pager
+  my $pager = '';
+  if ( $total != scalar(@$rows) && $maxrecords ) {
+    unless ( $offset == 0 ) {
+      $cgi->param('offset', $offset - $maxrecords);
+      $pager .= '<A HREF="'. $cgi->self_url.
+                '"><B><FONT SIZE="+1">Previous</FONT></B></A> ';
+    }
+    my $poff;
+    my $page;
+    for ( $poff = 0; $poff < $total; $poff += $maxrecords ) {
+      $page++;
+      if ( $offset == $poff ) {
+        $pager .= qq!<FONT SIZE="+2">$page</FONT> !;
+      } else {
+        $cgi->param('offset', $poff);
+        $pager .= qq!<A HREF="!. $cgi->self_url. qq!">$page</A> !;
+      }
+    }
+    unless ( $offset + $maxrecords > $total ) {
+      $cgi->param('offset', $offset + $maxrecords);
+      $pager .= '<A HREF="'. $cgi->self_url.
+                '"><B><FONT SIZE="+1">Next</FONT></B></A> ';
+    }
+  }
+  #end pager
+
+  print header('Query Results', menubar('Main Menu'=>$p) ).
+        "$total total rows<BR><BR>$pager". table().
+        "<TR>";
+  print "<TH>$_</TH>" foreach @{$sth->{NAME}};
+  print "</TR>";
+
+  foreach $row ( @$rows ) {
+    print "<TR>";
+    print "<TD>$_</TD>" foreach @$row;
+    print "</TR>";
+  }
+
+  print "</TABLE>$pager</BODY></HTML>";
+
+%>
diff --git a/httemplate/search/svc_acct.cgi b/httemplate/search/svc_acct.cgi
new file mode 100755 (executable)
index 0000000..e43f4f7
--- /dev/null
@@ -0,0 +1,277 @@
+<%
+
+my $conf = new FS::Conf;
+my $maxrecords = $conf->config('maxsearchrecordsperpage');
+
+my $orderby = ''; #removeme
+
+my $limit = '';
+$limit .= "LIMIT $maxrecords" if $maxrecords;
+
+my $offset = $cgi->param('offset') || 0;
+$limit .= " OFFSET $offset" if $offset;
+
+my $total;
+
+my($query)=$cgi->keywords;
+$query ||= ''; #to avoid use of unitialized value errors
+
+my $unlinked = '';
+if ( $query =~ /^UN_(.*)$/ ) {
+  $query = $1;
+  my $empty = driver_name eq 'Pg' ? qq('') : qq("");
+  if ( driver_name eq 'mysql' ) {
+    $unlinked = "LEFT JOIN cust_svc ON cust_svc.svcnum = svc_acct.svcnum
+                 WHERE cust_svc.pkgnum IS NULL
+                    OR cust_svc.pkgnum = 0
+                    OR cust_svc.pkgnum = $empty";
+  } else {
+    $unlinked = "
+      WHERE 0 <
+        ( SELECT count(*) FROM cust_svc
+            WHERE cust_svc.svcnum = svc_acct.svcnum
+              AND ( pkgnum IS NULL OR pkgnum = 0 )
+        )
+    ";
+  }
+}
+
+my $tblname = driver_name eq 'mysql' ? 'svc_acct.' : '';
+my(@svc_acct, $sortby);
+if ( $query eq 'svcnum' ) {
+  $sortby=\*svcnum_sort;
+  $orderby = "ORDER BY ${tblname}svcnum";
+} elsif ( $query eq 'username' ) {
+  $sortby=\*username_sort;
+  $orderby = "ORDER BY ${tblname}username";
+} elsif ( $query eq 'uid' ) {
+  $sortby=\*uid_sort;
+  $orderby = ( $unlinked ? 'AND' : 'WHERE' ).
+             " ${tblname}uid IS NOT NULL ORDER BY ${tblname}uid";
+} else {
+  $sortby=\*uid_sort;
+  @svc_acct = @{&usernamesearch};
+}
+
+if ( $query eq 'svcnum' || $query eq 'username' || $query eq 'uid' ) {
+
+  my $statement = "SELECT COUNT(*) FROM svc_acct $unlinked";
+  my $sth = dbh->prepare($statement)
+    or die dbh->errstr. " doing $statement";
+  $sth->execute or die "Error executing \"$statement\": ". $sth->errstr;
+
+  $total = $sth->fetchrow_arrayref->[0];
+
+  @svc_acct = qsearch('svc_acct', {}, '', "$unlinked $orderby $limit");
+
+}
+
+if ( scalar(@svc_acct) == 1 ) {
+  my($svcnum)=$svc_acct[0]->svcnum;
+  print $cgi->redirect(popurl(2). "view/svc_acct.cgi?$svcnum");  #redirect
+  #exit;
+} elsif ( scalar(@svc_acct) == 0 ) { #error
+%>
+<!-- mason kludge -->
+<%
+  idiot("Account not found");
+} else {
+%>
+<!-- mason kludge -->
+<%
+  $total ||= scalar(@svc_acct);
+
+  #begin pager
+  my $pager = '';
+  if ( $total != scalar(@svc_acct) && $maxrecords ) {
+    unless ( $offset == 0 ) {
+      $cgi->param('offset', $offset - $maxrecords);
+      $pager .= '<A HREF="'. $cgi->self_url.
+                '"><B><FONT SIZE="+1">Previous</FONT></B></A> ';
+    }
+    my $poff;
+    my $page;
+    for ( $poff = 0; $poff < $total; $poff += $maxrecords ) {
+      $page++;
+      if ( $offset == $poff ) {
+        $pager .= qq!<FONT SIZE="+2">$page</FONT> !;
+      } else {
+        $cgi->param('offset', $poff);
+        $pager .= qq!<A HREF="!. $cgi->self_url. qq!">$page</A> !;
+      }
+    }
+    unless ( $offset + $maxrecords > $total ) {
+      $cgi->param('offset', $offset + $maxrecords);
+      $pager .= '<A HREF="'. $cgi->self_url.
+                '"><B><FONT SIZE="+1">Next</FONT></B></A> ';
+    }
+  }
+  #end pager
+
+  print header("Account Search Results",menubar('Main Menu'=>popurl(2))),
+        "$total matching accounts found<BR><BR>$pager",
+        &table(), <<END;
+      <TR>
+        <TH><FONT SIZE=-1>#</FONT></TH>
+        <TH><FONT SIZE=-1>Username</FONT></TH>
+        <TH><FONT SIZE=-1>Domain</FONT></TH>
+        <TH><FONT SIZE=-1>UID</FONT></TH>
+        <TH><FONT SIZE=-1>Service</FONT></TH>
+        <TH><FONT SIZE=-1>Cust#</FONT></TH>
+        <TH><FONT SIZE=-1>(bill) name</FONT></TH>
+        <TH><FONT SIZE=-1>company</FONT></TH>
+END
+  if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+    print <<END;
+        <TH><FONT SIZE=-1>(service) name</FONT></TH>
+        <TH><FONT SIZE=-1>company</FONT></TH>
+END
+  }
+  print "</TR>";
+
+  my(%saw,$svc_acct);
+  my $p = popurl(2);
+  foreach $svc_acct (
+    sort $sortby grep(!$saw{$_->svcnum}++, @svc_acct)
+  ) {
+    my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svc_acct->svcnum })
+      or die "No cust_svc record for svcnum ". $svc_acct->svcnum;
+    my $part_svc = qsearchs('part_svc', { 'svcpart' => $cust_svc->svcpart })
+      or die "No part_svc record for svcpart ". $cust_svc->svcpart;
+
+    my $domain;
+    my $svc_domain = qsearchs('svc_domain', { 'svcnum' => $svc_acct->domsvc });
+    if ( $svc_domain ) {
+      $domain = "<A HREF=\"${p}view/svc_domain.cgi?". $svc_domain->svcnum.
+                "\">". $svc_domain->domain. "</A>";
+    } else {
+      die "No svc_domain.svcnum record for svc_acct.domsvc: ".
+          $svc_acct->domsvc;
+    }
+    my($cust_pkg,$cust_main);
+    if ( $cust_svc->pkgnum ) {
+      $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $cust_svc->pkgnum })
+        or die "No cust_pkg record for pkgnum ". $cust_svc->pkgnum;
+      $cust_main = qsearchs('cust_main', { 'custnum' => $cust_pkg->custnum })
+        or die "No cust_main record for custnum ". $cust_pkg->custnum;
+    }
+    my($svcnum, $username, $uid, $svc, $custnum, $last, $first, $company) = (
+      $svc_acct->svcnum,
+      $svc_acct->getfield('username'),
+      $svc_acct->getfield('uid'),
+      $part_svc->svc,
+      $cust_svc->pkgnum ? $cust_main->custnum : '',
+      $cust_svc->pkgnum ? $cust_main->getfield('last') : '',
+      $cust_svc->pkgnum ? $cust_main->getfield('first') : '',
+      $cust_svc->pkgnum ? $cust_main->company : '',
+    );
+    my($pcustnum) = $custnum
+      ? "<A HREF=\"${p}view/cust_main.cgi?$custnum\"><FONT SIZE=-1>$custnum</FONT></A>"
+      : "<I>(unlinked)</I>"
+    ;
+    my $pname = $custnum ? "<A HREF=\"${p}view/cust_main.cgi?$custnum\">$last, $first</A>" : '';
+    my $pcompany = $custnum ? "<A HREF=\"${p}view/cust_main.cgi?$custnum\">$company</A>" : '';
+    my($pship_name, $pship_company);
+    if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+      my($ship_last, $ship_first, $ship_company) = (
+        $cust_svc->pkgnum ? ( $cust_main->ship_last || $last ) : '',
+        $cust_svc->pkgnum ? ( $cust_main->ship_last
+                              ? $cust_main->ship_first
+                              : $first
+                            ) : '',
+        $cust_svc->pkgnum ? ( $cust_main->ship_last
+                              ? $cust_main->ship_company
+                              : $company
+                            ) : '',
+      );
+      $pship_name = $custnum ? "<A HREF=\"${p}view/cust_main.cgi?$custnum\">$ship_last, $ship_first</A>" : '';
+      $pship_company = $custnum ? "<A HREF=\"${p}view/cust_main.cgi?$custnum\">$ship_company</A>" : '';
+    }
+    print <<END;
+    <TR>
+      <TD><A HREF="${p}view/svc_acct.cgi?$svcnum"><FONT SIZE=-1>$svcnum</FONT></A></TD>
+      <TD><A HREF="${p}view/svc_acct.cgi?$svcnum"><FONT SIZE=-1>$username</FONT></A></TD>
+      <TD><FONT SIZE=-1>$domain</FONT></TD>
+      <TD><A HREF="${p}view/svc_acct.cgi?$svcnum"><FONT SIZE=-1>$uid</FONT></A></TD>
+      <TD><FONT SIZE=-1>$svc</FONT></TH>
+      <TD><FONT SIZE=-1>$pcustnum</FONT></TH>
+      <TD><FONT SIZE=-1>$pname<FONT></TH>
+      <TD><FONT SIZE=-1>$pcompany</FONT></TH>
+END
+    if ( defined dbdef->table('cust_main')->column('ship_last') ) {
+      print <<END;
+      <TD><FONT SIZE=-1>$pship_name<FONT></TH>
+      <TD><FONT SIZE=-1>$pship_company</FONT></TH>
+END
+    }
+    print "</TR>";
+
+  }
+  print "</TABLE>$pager<BR>".
+        '</BODY></HTML>';
+
+}
+
+sub svcnum_sort {
+  $a->getfield('svcnum') <=> $b->getfield('svcnum');
+}
+
+sub username_sort {
+  $a->getfield('username') cmp $b->getfield('username');
+}
+
+sub uid_sort {
+  $a->getfield('uid') <=> $b->getfield('uid');
+}
+
+sub usernamesearch {
+
+  my @svc_acct;
+
+  my %username_type;
+  foreach ( $cgi->param('username_type') ) {
+    $username_type{$_}++;
+  }
+
+  $cgi->param('username') =~ /^([\w\-\.\&]+)$/; #untaint username_text
+  my $username = $1;
+
+  if ( $username_type{'Exact'} || $username_type{'Fuzzy'} ) {
+    push @svc_acct, qsearch( 'svc_acct',
+                             { 'username' => { 'op'    => 'ILIKE',
+                                               'value' => $username } } );
+  }
+
+  if ( $username_type{'Substring'} || $username_type{'All'} ) {
+    push @svc_acct, qsearch( 'svc_acct',
+                             { 'username' => { 'op'    => 'ILIKE',
+                                               'value' => "%$username%" } } );
+  }
+
+  if ( $username_type{'Fuzzy'} || $username_type{'All'} ) {
+    &FS::svc_acct::check_and_rebuild_fuzzyfiles;
+    my $all_username = &FS::svc_acct::all_username;
+
+    my %username;
+    if ( $username_type{'Fuzzy'} || $username_type{'All'} ) { 
+      foreach ( amatch($username, [ qw(i) ], @$all_username) ) {
+        $username{$_}++; 
+      }
+    }
+
+    #if ($username_type{'Sound-alike'}) {
+    #}
+
+    foreach ( keys %username ) {
+      push @svc_acct, qsearch('svc_acct',{'username'=>$_});
+    }
+
+  }
+
+  #[ qsearch('svc_acct',{'username'=>$username}) ];
+  \@svc_acct;
+
+}
+
+%>
diff --git a/httemplate/search/svc_acct.html b/httemplate/search/svc_acct.html
new file mode 100755 (executable)
index 0000000..7423605
--- /dev/null
@@ -0,0 +1,19 @@
+<HTML>
+  <HEAD>
+    <TITLE>Account Search</TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <FONT SIZE=7>
+      Account Search
+    </FONT>
+    <BR><BR>
+    <FORM ACTION="svc_acct.cgi" METHOD="post">
+      Search for <B>username</B>: 
+      <INPUT TYPE="text" NAME="username">
+
+      <P><INPUT TYPE="submit" VALUE="Search">
+
+    </FORM>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/search/svc_domain.cgi b/httemplate/search/svc_domain.cgi
new file mode 100755 (executable)
index 0000000..c0acf11
--- /dev/null
@@ -0,0 +1,154 @@
+<%
+
+my $conf = new FS::Conf;
+
+my($query)=$cgi->keywords;
+$query ||= ''; #to avoid use of unitialized value errors
+my(@svc_domain,$sortby);
+if ( $query eq 'svcnum' ) {
+  $sortby=\*svcnum_sort;
+  @svc_domain=qsearch('svc_domain',{});
+} elsif ( $query eq 'domain' ) {
+  $sortby=\*domain_sort;
+  @svc_domain=qsearch('svc_domain',{});
+} elsif ( $query eq 'UN_svcnum' ) {
+  $sortby=\*svcnum_sort;
+  @svc_domain = grep qsearchs('cust_svc',{
+      'svcnum' => $_->svcnum,
+      'pkgnum' => '',
+    }), qsearch('svc_domain',{});
+} elsif ( $query eq 'UN_domain' ) {
+  $sortby=\*domain_sort;
+  @svc_domain = grep qsearchs('cust_svc',{
+      'svcnum' => $_->svcnum,
+      'pkgnum' => '',
+    }), qsearch('svc_domain',{});
+} else {
+  $cgi->param('domain') =~ /^([\w\-\.]+)$/; 
+  my($domain)=$1;
+  #push @svc_domain, qsearchs('svc_domain',{'domain'=>$domain});
+  @svc_domain = qsearchs('svc_domain',{'domain'=>$domain});
+}
+
+if ( scalar(@svc_domain) == 1 ) {
+  print $cgi->redirect(popurl(2). "view/svc_domain.cgi?". $svc_domain[0]->svcnum);
+  #exit;
+} elsif ( scalar(@svc_domain) == 0 ) {
+%>
+<!-- mason kludge -->
+<%
+  eidiot "No matching domains found!\n";
+} else {
+%>
+<!-- mason kludge -->
+<%
+  my($total)=scalar(@svc_domain);
+  print header("Domain Search Results",''), <<END;
+
+    $total matching domains found
+    <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
+      <TR>
+        <TH>Service #</TH>
+        <TH>Domain</TH>
+<!--        <TH>Mail to<BR><FONT SIZE=-1>(click to view account)</FONT></TH>
+        <TH>Forwards to<BR><FONT SIZE=-1>(click to view account)</FONT></TH>
+-->
+      </TR>
+END
+
+#  my(%saw);                 # if we've multiple domains with the same
+                             # svcnum, then we've a corrupt database
+
+  foreach my $svc_domain (
+#    sort $sortby grep(!$saw{$_->svcnum}++, @svc_domain)
+    sort $sortby (@svc_domain)
+  ) {
+    my($svcnum,$domain)=(
+      $svc_domain->svcnum,
+      $svc_domain->domain,
+    );
+
+    #don't display all accounts here
+    my $rowspan = 1;
+
+    #my @svc_acct=qsearch('svc_acct',{'domsvc' => $svcnum});
+    #my $rowspan = 0;
+    #
+    #my $n1 = '';
+    #my($svc_acct, @rows);
+    #foreach $svc_acct (
+    #  sort {$b->getfield('username') cmp $a->getfield('username')} (@svc_acct)
+    #) {
+    #
+    #  my (@forwards) = ();
+    #
+    #  my($svcnum,$username)=(
+    #    $svc_acct->svcnum,
+    #    $svc_acct->username,
+    #  );
+    #
+    #  my @svc_forward = qsearch( 'svc_forward', { 'srcsvc' => $svcnum } );
+    #  my $svc_forward;
+    #  foreach $svc_forward (@svc_forward) {
+    #    my($dstsvc,$dst) = (
+    #      $svc_forward->dstsvc,
+    #      $svc_forward->dst,
+    #    );
+    #    if ($dstsvc) {
+    #      my $dst_svc_acct=qsearchs( 'svc_acct', { 'svcnum' => $dstsvc } );
+    #      my $destination=$dst_svc_acct->email;
+    #      push @forwards, qq!<TD><A HREF="!, popurl(2),
+    #            qq!view/svc_acct.cgi?$dstsvc">$destination</A>!,
+    #            qq!</TD></TR>!
+    #      ;
+    #    }else{
+    #      push @forwards, qq!<TD>$dst</TD></TR>!
+    #      ;
+    #    }
+    #  }
+    #
+    #  push @rows, qq!$n1<TD ROWSPAN=!, (scalar(@svc_forward) || 1),
+    #        qq!><A HREF="!. popurl(2). qq!view/svc_acct.cgi?$svcnum">!,
+    #  #print '', ( ($domuser eq '*') ? "<I>(anything)</I>" : $domuser );
+    #        ( ($username eq '*') ? "<I>(anything)</I>" : $username ),
+    #        qq!\@$domain</A> </TD>!,
+    #  ;
+    #
+    #  push @rows, @forwards;
+    #
+    #  $rowspan += (scalar(@svc_forward) || 1);
+    #  $n1 = "</TR><TR>";
+    #}
+    ##end of false laziness
+    #
+    #
+
+    print <<END;
+    <TR>
+      <TD ROWSPAN=$rowspan><A HREF="${p}view/svc_domain.cgi?$svcnum">$svcnum</A></TD>
+      <TD ROWSPAN=$rowspan><A HREF="${p}view/svc_domain.cgi?$svcnum">$domain</A></TD>
+END
+
+    #print @rows;
+    print "</TR>";
+
+  }
+  print <<END;
+    </TABLE>
+  </BODY>
+</HTML>
+END
+
+}
+
+sub svcnum_sort {
+  $a->getfield('svcnum') <=> $b->getfield('svcnum');
+}
+
+sub domain_sort {
+  $a->getfield('domain') cmp $b->getfield('domain');
+}
+
+
+%>
diff --git a/httemplate/search/svc_domain.html b/httemplate/search/svc_domain.html
new file mode 100755 (executable)
index 0000000..94bb9a6
--- /dev/null
@@ -0,0 +1,19 @@
+<HTML>
+  <HEAD>
+    <TITLE>Domain Search</TITLE>
+  </HEAD>
+  <BODY BGCOLOR="#e8e8e8">
+    <FONT SIZE=7>
+      Domain Search
+    </FONT>
+    <BR><BR>
+    <FORM ACTION="svc_domain.cgi" METHOD="post">
+      Search for <B>domain</B>: 
+      <INPUT TYPE="text" NAME="domain">
+
+      <P><INPUT TYPE="submit" VALUE="Search">
+
+    </FORM>
+  </BODY>
+</HTML>
+
diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi
new file mode 100755 (executable)
index 0000000..53d7bc0
--- /dev/null
@@ -0,0 +1,48 @@
+<!-- mason kludge -->
+<%
+
+#untaint invnum
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $invnum = $1;
+
+my $cust_bill = qsearchs('cust_bill',{'invnum'=>$invnum});
+die "Invoice #$invnum not found!" unless $cust_bill;
+my $custnum = $cust_bill->getfield('custnum');
+
+#my $printed = $cust_bill->printed;
+
+print header('Invoice View', menubar(
+  "Main Menu" => $p,
+  "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+));
+
+print qq!<A HREF="${p}edit/cust_pay.cgi?$invnum">Enter payments (check/cash) against this invoice</A> | !
+  if $cust_bill->owed > 0;
+
+print qq!<A HREF="${p}misc/print-invoice.cgi?$invnum">Reprint this invoice</A>!.      '<BR><BR>';
+
+#false laziness with search/cust_bill_event.cgi
+
+print table(). '<TR><TH>Event</TH><TH>Date</TH><TH>Status</TH></TR>';
+foreach my $cust_bill_event (
+  sort { $a->_date <=> $b->_date } $cust_bill->cust_bill_event
+) {
+  my $status = $cust_bill_event->status;
+  $status .= ': '. $cust_bill_event->statustext if $cust_bill_event->statustext;
+  print '<TR><TD>'. $cust_bill_event->part_bill_event->event. '</TD><TD>'.
+        time2str("%a %b %e %T %Y", $cust_bill_event->_date). '</TD><TD>'.
+        $status. '</TD></TR>';
+}
+print '</TABLE><BR><PRE>';
+
+print $cust_bill->print_text;
+
+       #formatting
+       print <<END;
+    </PRE></FONT>
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi
new file mode 100755 (executable)
index 0000000..c36c9e2
--- /dev/null
@@ -0,0 +1,923 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+#false laziness with view/cust_pkg.cgi, but i'm trying to make that go away so
+my %uiview = ();
+my %uiadd = ();
+foreach my $part_svc ( qsearch('part_svc',{}) ) {
+  $uiview{$part_svc->svcpart} = popurl(2). "view/". $part_svc->svcdb . ".cgi";
+  $uiadd{$part_svc->svcpart}= popurl(2). "edit/". $part_svc->svcdb . ".cgi";
+}
+
+print header("Customer View", menubar(
+  'Main Menu' => popurl(2)
+));
+
+print <<END;
+<STYLE TYPE="text/css">
+.package TH { font-size: medium }
+.package TR { font-size: smaller }
+.package .pkgnum { font-size: medium }
+.package .provision { font-weight: bold }
+</STYLE>
+END
+
+die "No customer specified (bad URL)!" unless $cgi->keywords;
+my($query) = $cgi->keywords; # needs parens with my, ->keywords returns array
+$query =~ /^(\d+)$/;
+my $custnum = $1;
+my $cust_main = qsearchs('cust_main',{'custnum'=>$custnum});
+die "Customer not found!" unless $cust_main;
+
+print qq!<A HREF="${p}edit/cust_main.cgi?$custnum">Edit this customer</A>!;
+
+print <<END;
+<SCRIPT>
+function cancel_areyousure(href) {
+    if (confirm("Perminantly delete all services and cancel this customer?") == true)
+        window.location.href = href;
+}
+</SCRIPT>
+END
+
+print qq! | <A HREF="javascript:cancel_areyousure('${p}misc/cust_main-cancel.cgi?$custnum')">!.
+      'Cancel this customer</A>'
+  if $cust_main->ncancelled_pkgs;
+
+print qq! | <A HREF="${p}misc/delete-customer.cgi?$custnum">!.
+      'Delete this customer</A>'
+  if $conf->exists('deletecustomers');
+
+unless ( $conf->exists('disable_customer_referrals') ) {
+  print qq! | <A HREF="!, popurl(2),
+        qq!edit/cust_main.cgi?referral_custnum=$custnum">!,
+        qq!Refer a new customer</A>!;
+
+  print qq! | <A HREF="!, popurl(2),
+        qq!search/cust_main.cgi?referral_custnum=$custnum">!,
+        qq!View this customer's referrals</A>!;
+}
+
+print '<BR><BR>';
+
+my $signupurl = $conf->config('signupurl');
+if ( $signupurl ) {
+print "This customer's signup URL: ".
+      "<a href=\"$signupurl?ref=$custnum\">$signupurl?ref=$custnum</a><BR><BR>";
+}
+
+print '<A NAME="cust_main"></A>';
+
+print &itable(), '<TR>';
+
+print '<TD VALIGN="top">';
+
+  print "Billing address", &ntable("#cccccc"), "<TR><TD>",
+        &ntable("#cccccc",2),
+    '<TR><TD ALIGN="right">Contact name</TD>',
+      '<TD COLSPAN=3 BGCOLOR="#ffffff">',
+      $cust_main->last, ', ', $cust_main->first,
+      '</TD>';
+print '<TD ALIGN="right">SS#</TD><TD BGCOLOR="#ffffff">',
+      $cust_main->ss || '&nbsp', '</TD>'
+  if $conf->exists('show_ss');
+
+print '</TR>',
+    '<TR><TD ALIGN="right">Company</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+      $cust_main->company,
+      '</TD></TR>',
+    '<TR><TD ALIGN="right">Address</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+      $cust_main->address1,
+      '</TD></TR>',
+  ;
+  print '<TR><TD ALIGN="right">&nbsp;</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+        $cust_main->address2, '</TD></TR>'
+    if $cust_main->address2;
+  print '<TR><TD ALIGN="right">City</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->city,
+          '</TD><TD ALIGN="right">State</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->state,
+          '</TD><TD ALIGN="right">Zip</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->zip, '</TD></TR>',
+        '<TR><TD ALIGN="right">Country</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->country,
+          '</TD></TR>',
+  ;
+  my $daytime_label = FS::Msgcat::_gettext('daytime') || 'Day Phone';
+  my $night_label = FS::Msgcat::_gettext('night') || 'Night Phone';
+  print '<TR><TD ALIGN="right">'. $daytime_label.
+          '</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+          $cust_main->daytime || '&nbsp', '</TD></TR>',
+        '<TR><TD ALIGN="right">'. $night_label. 
+          '</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+          $cust_main->night || '&nbsp', '</TD></TR>',
+        '<TR><TD ALIGN="right">Fax</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+          $cust_main->fax || '&nbsp', '</TD></TR>',
+        '</TABLE>', "</TD></TR></TABLE>"
+  ;
+
+  if ( defined $cust_main->dbdef_table->column('ship_last') ) {
+
+    my $pre = $cust_main->ship_last ? 'ship_' : '';
+
+    print "<BR>Service address", &ntable("#cccccc"), "<TR><TD>",
+          &ntable("#cccccc",2),
+      '<TR><TD ALIGN="right">Contact name</TD>',
+        '<TD COLSPAN=5 BGCOLOR="#ffffff">',
+        $cust_main->get("${pre}last"), ', ', $cust_main->get("${pre}first"),
+        '</TD></TR>',
+      '<TR><TD ALIGN="right">Company</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+        $cust_main->get("${pre}company"),
+        '</TD></TR>',
+      '<TR><TD ALIGN="right">Address</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+        $cust_main->get("${pre}address1"),
+        '</TD></TR>',
+    ;
+    print '<TR><TD ALIGN="right">&nbsp;</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+          $cust_main->get("${pre}address2"), '</TD></TR>'
+      if $cust_main->get("${pre}address2");
+    print '<TR><TD ALIGN="right">City</TD><TD BGCOLOR="#ffffff">',
+            $cust_main->get("${pre}city"),
+            '</TD><TD ALIGN="right">State</TD><TD BGCOLOR="#ffffff">',
+            $cust_main->get("${pre}state"),
+            '</TD><TD ALIGN="right">Zip</TD><TD BGCOLOR="#ffffff">',
+            $cust_main->get("${pre}zip"), '</TD></TR>',
+          '<TR><TD ALIGN="right">Country</TD><TD BGCOLOR="#ffffff">',
+            $cust_main->get("${pre}country"),
+            '</TD></TR>',
+    ;
+    print '<TR><TD ALIGN="right">'. $daytime_label. '</TD>',
+          '<TD COLSPAN=5 BGCOLOR="#ffffff">',
+            $cust_main->get("${pre}daytime") || '&nbsp', '</TD></TR>',
+          '<TR><TD ALIGN="right">'. $night_label. '</TD>'.
+          '<TD COLSPAN=5 BGCOLOR="#ffffff">',
+            $cust_main->get("${pre}night") || '&nbsp', '</TD></TR>',
+          '<TR><TD ALIGN="right">Fax</TD><TD COLSPAN=5 BGCOLOR="#ffffff">',
+            $cust_main->get("${pre}fax") || '&nbsp', '</TD></TR>',
+          '</TABLE>', "</TD></TR></TABLE>"
+    ;
+
+  }
+
+print '</TD>';
+
+print '<TD VALIGN="top">';
+
+  print &ntable("#cccccc"), "<TR><TD>", &ntable("#cccccc",2),
+        '<TR><TD ALIGN="right">Customer number</TD><TD BGCOLOR="#ffffff">',
+        $custnum, '</TD></TR>',
+  ;
+
+  my @agents = qsearch( 'agent', {} );
+  my $agent;
+  unless ( scalar(@agents) == 1 ) {
+    $agent = qsearchs('agent',{ 'agentnum' => $cust_main->agentnum } );
+    print '<TR><TD ALIGN="right">Agent</TD><TD BGCOLOR="#ffffff">',
+        $agent->agentnum, ": ", $agent->agent, '</TD></TR>';
+  } else {
+    $agent = $agents[0];
+  }
+  my @referrals = qsearch( 'part_referral', {} );
+  unless ( scalar(@referrals) == 1 ) {
+    my $referral = qsearchs('part_referral', {
+      'refnum' => $cust_main->refnum
+    } );
+    print '<TR><TD ALIGN="right">Advertising source</TD><TD BGCOLOR="#ffffff">',
+          $referral->refnum, ": ", $referral->referral, '</TD></TR>';
+  }
+  print '<TR><TD ALIGN="right">Order taker</TD><TD BGCOLOR="#ffffff">',
+    $cust_main->otaker, '</TD></TR>';
+
+  print '<TR><TD ALIGN="right">Referring Customer</TD><TD BGCOLOR="#ffffff">';
+  my $referring_cust_main = '';
+  if ( $cust_main->referral_custnum
+       && ( $referring_cust_main =
+            qsearchs('cust_main', { custnum => $cust_main->referral_custnum } )
+          )
+     ) {
+    print '<A HREF="'. popurl(1). 'cust_main.cgi?'.
+          $cust_main->referral_custnum. '">'.
+          $cust_main->referral_custnum. ': '.
+          ( $referring_cust_main->company
+              ? $referring_cust_main->company. ' ('.
+                  $referring_cust_main->last. ', '. $referring_cust_main->first.
+                  ')'
+              : $referring_cust_main->last. ', '. $referring_cust_main->first
+          ).
+          '</A>';
+  }
+  print '</TD></TR>';
+
+  print '</TABLE></TD></TR></TABLE>';
+
+print '<BR>';
+
+if ( $conf->config('payby-default') ne 'HIDE' ) {
+
+  my @invoicing_list = $cust_main->invoicing_list;
+  print "Billing information (",
+       qq!<A HREF="!, popurl(2), qq!misc/bill.cgi?$custnum">!, "Bill now</A>)",
+        &ntable("#cccccc"), "<TR><TD>", &ntable("#cccccc",2),
+        '<TR><TD ALIGN="right">Tax exempt</TD><TD BGCOLOR="#ffffff">',
+        $cust_main->tax ? 'yes' : 'no',
+        '</TD></TR>',
+        '<TR><TD ALIGN="right">Postal invoices</TD><TD BGCOLOR="#ffffff">',
+        ( grep { $_ eq 'POST' } @invoicing_list ) ? 'yes' : 'no',
+        '</TD></TR>',
+        '<TR><TD ALIGN="right">Email invoices</TD><TD BGCOLOR="#ffffff">',
+        join(', ', grep { $_ ne 'POST' } @invoicing_list ) || 'no',
+        '</TD></TR>',
+        '<TR><TD ALIGN="right">Billing type</TD><TD BGCOLOR="#ffffff">',
+  ;
+
+  if ( $cust_main->payby eq 'CARD' || $cust_main->payby eq 'DCRD' ) {
+    my $payinfo = $cust_main->payinfo;
+    $payinfo = 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
+    print 'Credit card ',
+          ( $cust_main->payby eq 'CARD' ? '(automatic)' : '(on-demand)' ),
+          '</TD></TR>',
+          '<TR><TD ALIGN="right">Card number</TD><TD BGCOLOR="#ffffff">',
+          $payinfo, '</TD></TR>',
+          '<TR><TD ALIGN="right">Expiration</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->paydate, '</TD></TR>',
+          '<TR><TD ALIGN="right">Name on card</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->payname, '</TD></TR>'
+    ;
+  } elsif ( $cust_main->payby eq 'CHEK' || $cust_main->payby eq 'DCHK') {
+    my( $account, $aba ) = split('@', $cust_main->payinfo );
+    print 'Electronic check',
+          ( $cust_main->payby eq 'CHEK' ? '(automatic)' : '(on-demand)' ),
+          '</TD></TR>',
+          '<TR><TD ALIGN="right">Account number</TD><TD BGCOLOR="#ffffff">',
+          $account, '</TD></TR>',
+          '<TR><TD ALIGN="right">ABA/Routing code</TD><TD BGCOLOR="#ffffff">',
+          $aba, '</TD></TR>',
+          '<TR><TD ALIGN="right">Bank name</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->payname, '</TD></TR>'
+    ;
+  } elsif ( $cust_main->payby eq 'LECB' ) {
+    $cust_main->payinfo =~ /^(\d{3})(\d{3})(\d{4})$/;
+    my $payinfo = "$1-$2-$3";
+    print 'Phone bill billing</TD></TR>',
+          '<TR><TD ALIGN="right">Phone number</TD><TD BGCOLOR="#ffffff">',
+          $payinfo, '</TD></TR>',
+    ;
+  } elsif ( $cust_main->payby eq 'BILL' ) {
+    print 'Billing</TD></TR>';
+    print '<TR><TD ALIGN="right">P.O. </TD><TD BGCOLOR="#ffffff">',
+          $cust_main->payinfo, '</TD></TR>',
+      if $cust_main->payinfo;
+    print '<TR><TD ALIGN="right">Expiration</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->paydate, '</TD></TR>',
+          '<TR><TD ALIGN="right">Attention</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->payname, '</TD></TR>',
+    ;
+  } elsif ( $cust_main->payby eq 'COMP' ) {
+    print 'Complimentary</TD></TR>',
+          '<TR><TD ALIGN="right">Authorized by</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->payinfo, '</TD></TR>',
+          '<TR><TD ALIGN="right">Expiration</TD><TD BGCOLOR="#ffffff">',
+          $cust_main->paydate, '</TD></TR>',
+    ;
+  }
+
+  print "</TABLE></TD></TR></TABLE>";
+
+}
+
+print '</TD></TR></TABLE>';
+
+if ( defined $cust_main->dbdef_table->column('comments')
+     && $cust_main->comments =~ /[^\s\n\r]/ )
+{
+  print "<BR>Comments". &ntable("#cccccc"). "<TR><TD>".
+        &ntable("#cccccc",2).
+        '<TR><TD BGCOLOR="#ffffff"><PRE>'.
+        encode_entities($cust_main->comments).
+        '</PRE></TD></TR></TABLE></TABLE>';
+}
+
+print '</TD></TR></TABLE>';
+
+print '<BR>'.
+  '<FORM ACTION="'.popurl(2).'edit/process/quick-cust_pkg.cgi" METHOD="POST">'.
+  qq!<INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!.
+  '<SELECT NAME="pkgpart"><OPTION> ';
+
+foreach my $part_pkg (
+  qsearch( 'part_pkg', { 'disabled' => '' }, '',
+           ' AND 0 < ( SELECT COUNT(*) FROM type_pkgs '.
+           '             WHERE typenum = '. $agent->typenum.
+           '             AND type_pkgs.pkgpart = part_pkg.pkgpart )'
+         )
+) {
+  print '<OPTION VALUE="'. $part_pkg->pkgpart. '">'. $part_pkg->pkg. ' - '.
+        $part_pkg->comment;
+}
+
+print '</SELECT><INPUT TYPE="submit" VALUE="Order Package"></FORM><BR>';
+
+if ( $conf->config('payby-default') ne 'HIDE' ) {
+
+  print '<BR>'.
+    qq!<FORM ACTION="${p}edit/process/quick-charge.cgi" METHOD="POST">!.
+    qq!<INPUT TYPE="hidden" NAME="custnum" VALUE="$custnum">!.
+    qq!Description:<INPUT TYPE="text" NAME="pkg">!.
+    qq!&nbsp;Amount:<INPUT TYPE="text" NAME="amount" SIZE=6>!.
+    qq!&nbsp;!;
+  
+  #false laziness w/ edit/part_pkg.cgi
+  if ( $conf->exists('enable_taxclasses') ) {
+    print '<SELECT NAME="taxclass">';
+    my $sth = dbh->prepare('SELECT DISTINCT taxclass FROM cust_main_county')
+      or die dbh->errstr;
+    $sth->execute or die $sth->errstr;
+    foreach my $taxclass ( map $_->[0], @{$sth->fetchall_arrayref} ) {
+      print qq!<OPTION VALUE="$taxclass"!;
+      #print ' SELECTED' if $taxclass eq $hashref->{taxclass};
+      print qq!>$taxclass</OPTION>!;
+    }
+    print '</SELECT>';
+  } else {
+    print '<INPUT TYPE="hidden" NAME="taxclass" VALUE="">';
+  }
+  
+  print qq!<INPUT TYPE="submit" VALUE="One-time charge"></FORM><BR>!;
+
+}
+
+print <<END;
+<SCRIPT>
+function cust_pkg_areyousure(href) {
+    if (confirm("Permanently delete included services and cancel this package?") == true)
+        window.location.href = href;
+}
+function svc_areyousure(href) {
+    if (confirm("Permanently unprovision and delete this service?") == true)
+        window.location.href = href;
+}
+</SCRIPT>
+END
+
+print qq!<BR><A NAME="cust_pkg">Packages</A> !,
+#      qq!<BR>Click on package number to view/edit package.!,
+      qq!( <A HREF="!, popurl(2), qq!edit/cust_pkg.cgi?$custnum">Order and cancel packages</A> (preserves services) )!,
+;
+
+#begin display packages
+
+#get package info
+
+my $packages = get_packages($cust_main);
+
+if ( @$packages ) {
+%>
+<TABLE CLASS="package" BORDER=1 CELLSPACING=0 CELLPADDING=2 BORDERCOLOR="#999999">
+<TR>
+  <TH COLSPAN=2>Package</TH>
+  <TH>Status</TH>
+  <TH COLSPAN=2>Services</TH>
+</TR>
+<%
+foreach my $pkg (sort pkgsort_pkgnum_cancel @$packages) {
+  my $rowspan = 0;
+
+  if ($pkg->{cancel}) {
+    $rowspan = 0;
+  } else {
+    foreach my $svcpart (@{$pkg->{svcparts}}) {
+      $rowspan += $svcpart->{count};
+      $rowspan++ if ($svcpart->{count} < $svcpart->{quantity});
+    }
+  } 
+
+%>
+<!--pkgnum: <%=$pkg->{pkgnum}%>-->
+<TR>
+  <TD ROWSPAN=<%=$rowspan%> CLASS="pkgnum"><%=$pkg->{pkgnum}%></TD>
+  <TD ROWSPAN=<%=$rowspan%>>
+    <%=$pkg->{pkg}%> - <%=$pkg->{comment}%> (&nbsp;<%=pkg_details_link($pkg)%>&nbsp;)<BR>
+<% unless ($pkg->{cancel}) { %>
+    (&nbsp;<%=pkg_change_link($pkg)%>&nbsp;)
+    (&nbsp;<%=pkg_dates_link($pkg)%>&nbsp;|&nbsp;<%=pkg_customize_link($pkg)%>&nbsp;)
+<% } %>
+  </TD>
+<%
+  #foreach (qw(setup last_bill next_bill susp expire cancel)) {
+  #  print qq!  <TD ROWSPAN=$rowspan>! . pkg_datestr($pkg,$_) . qq!</TD>\n!;
+  #}
+  print "<TD ROWSPAN=$rowspan>". &itable('');
+
+  #move
+  my %freq = (
+    1 => 'monthly',
+    2 => 'bi-monthly',
+    3 => 'quarterly',
+    6 => 'semi-annually',
+    12 => 'annually',
+    24 => 'bi-annually',
+    36 => 'tri-annually',
+  );
+
+  sub freq {
+    my $freq = shift;
+    exists $freq{$freq} ? $freq{$freq} : "every&nbsp;$freq&nbsp;months";
+  }
+
+  #eomove
+
+  if ( $pkg->{cancel} ) { #status: cancelled
+
+    print '<TR><TD><FONT COLOR="#ff0000"><B>Cancelled&nbsp;</B></FONT></TD>'.
+          '<TD>'. pkg_datestr($pkg,'cancel'). '</TD></TR>';
+    unless ( $pkg->{setup} ) {
+      print '<TR><TD COLSPAN=2>Never billed</TD></TR>';
+    } else {
+      print "<TR><TD>Setup&nbsp;</TD><TD>".
+            pkg_datestr($pkg, 'setup'). '</TD></TR>';
+      print "<TR><TD>Last&nbsp;bill&nbsp;</TD><TD>".
+            pkg_datestr($pkg, 'last_bill'). '</TD></TR>'
+        if $pkg->{'last_bill'};
+      print "<TR><TD>Suspended&nbsp;</TD><TD>".
+            pkg_datestr($pkg, 'susp'). '</TD></TR>'
+        if $pkg->{'susp'};
+    }
+
+  } else {
+
+    if ( $pkg->{susp} ) { #status: suspended
+      print '<TR><TD><FONT COLOR="#FF9900"><B>Suspended</B>&nbsp;</FONT></TD>'.
+            '<TD>'. pkg_datestr($pkg,'susp'). '</TD></TR>';
+      unless ( $pkg->{setup} ) {
+        print '<TR><TD COLSPAN=2>Never billed</TD></TR>';
+      } else {
+        print "<TR><TD>Setup&nbsp;</TD><TD>". 
+              pkg_datestr($pkg, 'setup'). '</TD></TR>';
+      }
+      print "<TR><TD>Last&nbsp;bill&nbsp;</TD><TD>".
+            pkg_datestr($pkg, 'last_bill'). '</TD></TR>'
+        if $pkg->{'last_bill'};
+      # next bill ??
+      print "<TR><TD>Expires&nbsp;</TD><TD>".
+            pkg_datestr($pkg, 'expire'). '</TD></TR>'
+        if $pkg->{'expire'};
+      print '<TR><TD COLSPAN=2>(&nbsp;'. pkg_unsuspend_link($pkg).
+            '&nbsp;|&nbsp;'. pkg_cancel_link($pkg). '&nbsp;)</TD></TR>';
+
+    } else { #status: active
+
+      unless ( $pkg->{setup} ) { #not setup
+
+        print '<TR><TD COLSPAN=2>Not&nbsp;yet&nbsp;billed&nbsp;(';
+        unless ( $pkg->{freq} ) {
+          print 'one-time&nbsp;charge)</TD></TR>';
+          print '<TR><TD COLSPAN=2>(&nbsp;'. pkg_cancel_link($pkg).
+                '&nbsp;)</TD</TR>';
+        } else {
+          print 'billed&nbsp;'. freq($pkg->{freq}). ')</TD></TR>';
+        }
+
+      } else { #setup
+
+        unless ( $pkg->{freq} ) {
+          print "<TR><TD COLSPAN=2>One-time&nbsp;charge</TD></TR>".
+                '<TR><TD>Billed&nbsp;</TD><TD>'.
+                pkg_datestr($pkg,'setup'). '</TD></TR>';
+        } else {
+          print '<TR><TD COLSPAN=2><FONT COLOR="#00CC00"><B>Active</B></FONT>'.
+                ',&nbsp;billed&nbsp;'. freq($pkg->{freq}). '</TD></TR>'.
+                '<TR><TD>Setup&nbsp;</TD><TD>'.
+                pkg_datestr($pkg, 'setup'). '</TD></TR>';
+        }
+
+      }
+
+      print "<TR><TD>Last&nbsp;bill&nbsp;</TD><TD>".
+            pkg_datestr($pkg, 'last_bill'). '</TD></TR>'
+        if $pkg->{'last_bill'};
+      print "<TR><TD>Next&nbsp;bill&nbsp;</TD><TD>".
+            pkg_datestr($pkg, 'next_bill'). '</TD></TR>'
+        if $pkg->{'next_bill'};
+      print "<TR><TD>Expires&nbsp;</TD><TD>".
+            pkg_datestr($pkg, 'expire'). '</TD></TR>'
+        if $pkg->{'expire'};
+      if ( $pkg->{freq} ) {
+        print '<TR><TD COLSPAN=2>(&nbsp;'. pkg_suspend_link($pkg).
+              '&nbsp;|&nbsp;'. pkg_cancel_link($pkg). '&nbsp;)</TD></TR>';
+      }
+
+    }
+
+  }
+
+  print "</TABLE></TD>\n";
+
+  if ($rowspan == 0) { print qq!</TR>\n!; next; }
+
+  my $cnt = 0;
+  foreach my $svcpart (sort {$a->{svcpart} <=> $b->{svcpart}} @{$pkg->{svcparts}}) {
+    foreach my $service (@{$svcpart->{services}}) {
+      print '<TR>' if ($cnt > 0);
+%>
+  <TD><%=svc_link($svcpart,$service)%></TD>
+  <TD><%=svc_label_link($svcpart,$service)%><BR>(&nbsp;<%=svc_unprovision_link($service)%>&nbsp;)</TD>
+</TR>
+<%
+      $cnt++;
+    }
+    if ($svcpart->{count} < $svcpart->{quantity}) {
+      print qq!<TR>\n! if ($cnt > 0);
+      print qq!  <TD COLSPAN=2>!.svc_provision_link($pkg,$svcpart).qq!</TD>\n</TR>\n!;
+    }
+  }
+}
+print '</TABLE>'
+}
+
+#end display packages
+
+
+print <<END;
+<SCRIPT>
+function cust_pay_areyousure(href) {
+    if (confirm("Are you sure you want to delete this payment?")
+ == true)
+        window.location.href = href;
+}
+function cust_pay_unapply_areyousure(href) {
+    if (confirm("Are you sure you want to unapply this payment?")
+ == true)
+        window.location.href = href;
+}
+</SCRIPT>
+END
+
+if ( $conf->config('payby-default') ne 'HIDE' ) {
+  
+  #formatting
+  print qq!<BR><BR><A NAME="history">Payment History!.
+        qq!</A> ( !.
+        qq!<A HREF="!. popurl(2). qq!edit/cust_pay.cgi?custnum=$custnum">!.
+        qq!Post payment</A> | !.
+        qq!<A HREF="!. popurl(2). qq!edit/cust_credit.cgi?$custnum">!.
+        qq!Post credit</A> )!;
+  
+  #get payment history
+  #
+  # major problem: this whole thing is way too sloppy.
+  # minor problem: the description lines need better formatting.
+  
+  my @history = (); #needed for mod_perl :)
+  
+  my %target = ();
+  
+  my @bills = qsearch('cust_bill',{'custnum'=>$custnum});
+  foreach my $bill (@bills) {
+    my($bref)=$bill->hashref;
+    my $bpre = ( $bill->owed > 0 )
+                 ? '<b><font size="+1" color="#ff0000"> Open '
+                 : '';
+    my $bpost = ( $bill->owed > 0 ) ? '</font></b>' : '';
+    push @history,
+      $bref->{_date} . qq!\t<A HREF="!. popurl(2). qq!view/cust_bill.cgi?! .
+      $bref->{invnum} . qq!">${bpre}Invoice #! . $bref->{invnum} .
+      qq! (Balance \$! . $bill->owed . qq!)$bpost</A>\t! .
+      $bref->{charged} . qq!\t\t\t!;
+  
+    my(@cust_bill_pay)=qsearch('cust_bill_pay',{'invnum'=> $bref->{invnum} } );
+  #  my(@payments)=qsearch('cust_pay',{'invnum'=> $bref->{invnum} } );
+  #  my($payment);
+    foreach my $cust_bill_pay (@cust_bill_pay) {
+      my $payment = $cust_bill_pay->cust_pay;
+      my($date,$invnum,$payby,$payinfo,$paid)=($payment->_date,
+                                               $cust_bill_pay->invnum,
+                                               $payment->payby,
+                                               $payment->payinfo,
+                                               $cust_bill_pay->amount,
+                        );
+      $payinfo = 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4))
+        if $payby eq 'CARD';
+      my $target = "$payby$payinfo";
+      $payby =~ s/^BILL$/Check #/ if $payinfo;
+      $payby =~ s/^(CARD|COMP)$/$1 /;
+      my $delete = $payment->closed !~ /^Y/i && $conf->exists('deletepayments')
+                     ? qq! (<A HREF="javascript:cust_pay_areyousure('${p}misc/delete-cust_pay.cgi?!. $payment->paynum. qq!')">delete</A>)!
+                     : '';
+      my $unapply =
+        $payment->closed !~ /^Y/i && $conf->exists('unapplypayments')
+          ? qq! (<A HREF="javascript:cust_pay_unapply_areyousure('${p}misc/unapply-cust_pay.cgi?!. $payment->paynum. qq!')">unapply</A>)!
+          : '';
+      push @history,
+        "$date\tPayment, Invoice #$invnum ($payby$payinfo)$delete$unapply\t\t$paid\t\t\t$target";
+    }
+  
+    my(@cust_credit_bill)=
+      qsearch('cust_credit_bill', { 'invnum'=> $bref->{invnum} } );
+    foreach my $cust_credit_bill (@cust_credit_bill) {
+      my $cust_credit = $cust_credit_bill->cust_credit;
+      my($date, $invnum, $crednum, $amount, $reason, $app_date ) = (
+        $cust_credit->_date,
+        $cust_credit_bill->invnum,
+        $cust_credit_bill->crednum,
+        $cust_credit_bill->amount,
+        $cust_credit->reason,
+        time2str("%D", $cust_credit_bill->_date),
+      );
+      push @history,
+        "$date\tCredit #$crednum: $reason<BR>".
+        "(applied to invoice #$invnum on $app_date)\t\t\t$amount\t";
+    }
+  }
+  
+  my @credits = grep { scalar(my @array = $_->cust_credit_refund) }
+             qsearch('cust_credit',{'custnum'=>$custnum});
+  foreach my $credit (@credits) {
+    my($cref)=$credit->hashref;
+    my(@cust_credit_refund)=
+      qsearch('cust_credit_refund', { 'crednum'=> $cref->{crednum} } );
+    foreach my $cust_credit_refund (@cust_credit_refund) {
+      my $cust_refund = $cust_credit_refund->cust_credit;
+      my($date, $crednum, $amount, $reason, $app_date ) = (
+        $credit->_date,
+        $credit->crednum,
+        $cust_credit_refund->amount,
+        $credit->reason,
+        time2str("%D", $cust_credit_refund->_date),
+      );
+      push @history,
+        "$date\tCredit #$crednum: $reason<BR>".
+        "(applied to refund on $app_date)\t\t\t$amount\t";
+    }
+  }
+  
+  @credits = grep { $_->credited  > 0 }
+             qsearch('cust_credit',{'custnum'=>$custnum});
+  foreach my $credit (@credits) {
+    my($cref)=$credit->hashref;
+    push @history,
+      $cref->{_date} . "\t" .
+      qq!<A HREF="! . popurl(2). qq!edit/cust_credit_bill.cgi?!. $cref->{crednum} . qq!">!.
+      '<b><font size="+1" color="#ff0000">Unapplied credit #' .
+      $cref->{crednum} . "</font></b></A>: ".
+      $cref->{reason} . "\t\t\t" . $credit->credited . "\t";
+  }
+  
+  my(@refunds)=qsearch('cust_refund',{'custnum'=> $custnum } );
+  foreach my $refund (@refunds) {
+    my($rref)=$refund->hashref;
+    my($refundnum) = (
+      $refund->refundnum,
+    );
+  
+    push @history,
+      $rref->{_date} . "\tRefund #$refundnum, (" .
+      $rref->{payby} . " " . $rref->{payinfo} . ") by " .
+      $rref->{otaker} . " - ". $rref->{reason} . "\t\t\t\t" .
+      $rref->{refund};
+  }
+  
+  my @unapplied_payments =
+    grep { $_->unapplied > 0 } qsearch('cust_pay', { 'custnum' => $custnum } );
+  foreach my $payment (@unapplied_payments) {
+    my $payby = $payment->payby;
+    my $payinfo = $payment->payinfo;
+    #false laziness w/above
+    $payinfo = 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4))
+      if $payby eq 'CARD';
+    my $target = "$payby$payinfo";
+    $payby =~ s/^BILL$/Check #/ if $payinfo;
+    $payby =~ s/^(CARD|COMP)$/$1 /;
+    my $delete = $payment->closed !~ /^Y/i && $conf->exists('deletepayments')
+                   ? qq! (<A HREF="javascript:cust_pay_areyousure('${p}misc/delete-cust_pay.cgi?!. $payment->paynum. qq!')">delete</A>)!
+                   : '';
+    push @history,
+      $payment->_date. "\t".
+      '<b><font size="+1" color="#ff0000">Unapplied payment #' .
+      $payment->paynum . " ($payby$payinfo)</font></b> ".
+      '(<A HREF="'. popurl(2). 'edit/cust_bill_pay.cgi?'. $payment->paynum. '">'.
+      "apply</A>)$delete".
+      "\t\t" . $payment->unapplied . "\t\t\t$target";
+  }
+  
+          #formatting
+          print &table(), <<END;
+  <TR>
+    <TH>Date</TH>
+    <TH>Description</TH>
+    <TH><FONT SIZE=-1>Charge</FONT></TH>
+    <TH><FONT SIZE=-1>Payment</FONT></TH>
+    <TH><FONT SIZE=-1>In-house<BR>Credit</FONT></TH>
+    <TH><FONT SIZE=-1>Refund</FONT></TH>
+    <TH><FONT SIZE=-1>Balance</FONT></TH>
+  </TR>
+END
+  
+  #display payment history
+  
+  my $balance = 0;
+  foreach my $item (sort keyfield_numerically @history) {
+    my($date,$desc,$charge,$payment,$credit,$refund,$target)=split(/\t/,$item);
+    $charge ||= 0;
+    $payment ||= 0;
+    $credit ||= 0;
+    $refund ||= 0;
+    $balance += $charge - $payment;
+    $balance -= $credit - $refund;
+    $balance = sprintf("%.2f", $balance);
+    $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
+    $target = '' unless defined $target;
+  
+    print "<TR><TD><FONT SIZE=-1>";
+    print qq!<A NAME="$target">! unless $target && $target{$target}++;
+    print time2str("%D",$date);
+    print '</A>' if $target && $target{$target} == 1;
+    print "</FONT></TD>",
+       "<TD><FONT SIZE=-1>$desc</FONT></TD>",
+       "<TD><FONT SIZE=-1>",
+          ( $charge ? "\$".sprintf("%.2f",$charge) : '' ),
+          "</FONT></TD>",
+       "<TD><FONT SIZE=-1>",
+          ( $payment ? "-&nbsp;\$".sprintf("%.2f",$payment) : '' ),
+          "</FONT></TD>",
+       "<TD><FONT SIZE=-1>",
+          ( $credit ? "-&nbsp;\$".sprintf("%.2f",$credit) : '' ),
+          "</FONT></TD>",
+       "<TD><FONT SIZE=-1>",
+          ( $refund ? "\$".sprintf("%.2f",$refund) : '' ),
+          "</FONT></TD>",
+       "<TD><FONT SIZE=-1>\$" . $balance,
+          "</FONT></TD>",
+          "\n";
+  }
+  
+  print "</TABLE>";
+
+}
+
+print '</BODY></HTML>';
+
+#subroutiens
+sub keyfield_numerically { (split(/\t/,$a))[0] <=> (split(/\t/,$b))[0]; }
+
+%>
+
+<%
+
+
+sub get_packages {
+
+my $cust_main = shift or return undef;
+
+my @packages = ();
+
+foreach my $cust_pkg (($conf->exists('hidecancelledpackages') ? ($cust_main->ncancelled_pkgs)
+                                                              : ($cust_main->all_pkgs))) { 
+
+  my $part_pkg = $cust_pkg->part_pkg;
+
+  my %pkg = ();
+  $pkg{pkgnum} = $cust_pkg->pkgnum;
+  $pkg{pkg} = $part_pkg->pkg;
+  $pkg{pkgpart} = $part_pkg->pkgpart;
+  $pkg{comment} = $part_pkg->getfield('comment');
+  $pkg{freq} = $part_pkg->freq;
+  $pkg{setup} = $cust_pkg->getfield('setup');
+  $pkg{last_bill} = $cust_pkg->getfield('last_bill');
+  $pkg{next_bill} = $cust_pkg->getfield('bill');
+  $pkg{susp} = $cust_pkg->getfield('susp');
+  $pkg{expire} = $cust_pkg->getfield('expire');
+  $pkg{cancel} = $cust_pkg->getfield('cancel');
+
+  $pkg{svcparts} = []; 
+
+  foreach my $pkg_svc (qsearch('pkg_svc', { 'pkgpart' => $part_pkg->pkgpart })) {
+
+    next if ($pkg_svc->quantity == 0);
+
+    my $part_svc = qsearchs('part_svc', { 'svcpart' => $pkg_svc->svcpart });
+
+    my $svcpart = {};
+    $svcpart->{svcpart} = $part_svc->svcpart;
+    $svcpart->{svc} = $part_svc->svc;
+    $svcpart->{svcdb} = $part_svc->svcdb;
+    $svcpart->{quantity} = $pkg_svc->quantity;
+    $svcpart->{count} = 0;
+
+    $svcpart->{services} = [];
+
+    foreach my $cust_svc (qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum,
+                                                'svcpart' => $part_svc->svcpart } )) {
+
+      my $svc = {};
+      $svc->{svcnum} = $cust_svc->svcnum;
+      $svc->{label} = ($cust_svc->label)[1];
+
+      push @{$svcpart->{services}}, $svc;
+
+      $svcpart->{count}++;
+
+    }
+
+    push @{$pkg{svcparts}}, $svcpart;
+
+  }
+
+  push @packages, \%pkg;
+
+}
+
+return \@packages;
+
+}
+
+sub svc_link {
+
+ my ($svcpart, $svc) = (shift,shift) or return '';
+ return qq!<A HREF="${p}view/$svcpart->{svcdb}.cgi?$svc->{svcnum}">$svcpart->{svc}</A>!;
+
+}
+
+sub svc_label_link {
+
+ my ($svcpart, $svc) = (shift,shift) or return '';
+ return qq!<A HREF="${p}view/$svcpart->{svcdb}.cgi?$svc->{svcnum}">$svc->{label}</A>!;
+
+}
+
+sub svc_provision_link {
+  my ($pkg, $svcpart) = (shift,shift) or return '';
+  ( my $svc_nbsp = $svcpart->{svc} ) =~ s/\s+/&nbsp;/g;
+  return qq!<A CLASS="provision" HREF="${p}edit/$svcpart->{svcdb}.cgi?! .
+         qq!pkgnum$pkg->{pkgnum}-svcpart$svcpart->{svcpart}">! .
+         "Provision&nbsp;$svc_nbsp&nbsp;(".
+         ($svcpart->{quantity} - $svcpart->{count}).
+         ')</A>';
+}
+
+sub svc_unprovision_link {
+  my $svc = shift or return '';
+  return qq!<A HREF="javascript:svc_areyousure('${p}misc/unprovision.cgi?$svc->{svcnum}')">Unprovision</A>!;
+}
+
+# This should be generalized to use config options to determine order.
+sub pkgsort_pkgnum_cancel {
+  if ($a->{cancel} and $b->{cancel}) {
+    return ($a->{pkgnum} <=> $b->{pkgnum});
+  } elsif ($a->{cancel} or $b->{cancel}) {
+    return (-1) if ($b->{cancel});
+    return (1) if ($a->{cancel});
+    return (0);
+  } else {
+    return($a->{pkgnum} <=> $b->{pkgnum});
+  }
+}
+
+sub pkg_datestr {
+  my($pkg, $field) = @_ or return '';
+  return '&nbsp;' unless $pkg->{$field};
+  my $format = $conf->exists('pkg_showtimes')
+               ? '<B>%D</B>&nbsp;<FONT SIZE=-3>%l:%M:%S%P&nbsp;%z</FONT>'
+               : '<B>%b&nbsp;%o,&nbsp;%Y</B>';
+  ( my $strip = time2str($format, $pkg->{$field}) ) =~ s/ (\d)/$1/g;
+  $strip;
+}
+
+sub pkg_details_link {
+  my $pkg = shift or return '';
+  return qq!<a href="${p}view/cust_pkg.cgi?$pkg->{pkgnum}">Details</a>!;
+}
+
+sub pkg_change_link {
+  my $pkg = shift or return '';
+  return qq!<a href="${p}misc/change_pkg.cgi?$pkg->{pkgnum}">Change&nbsp;package</a>!;
+}
+
+sub pkg_suspend_link {
+  my $pkg = shift or return '';
+  return qq!<a href="${p}misc/susp_pkg.cgi?$pkg->{pkgnum}">Suspend</a>!;
+}
+
+sub pkg_unsuspend_link {
+  my $pkg = shift or return '';
+  return qq!<a href="${p}misc/unsusp_pkg.cgi?$pkg->{pkgnum}">Unsuspend</a>!;
+}
+
+sub pkg_cancel_link {
+  my $pkg = shift or return '';
+  return qq!<A HREF="javascript:cust_pkg_areyousure('${p}misc/cancel_pkg.cgi?$pkg->{pkgnum}')">Cancel</A>!;
+}
+
+sub pkg_dates_link {
+  my $pkg = shift or return '';
+  return qq!<A HREF="${p}edit/REAL_cust_pkg.cgi?$pkg->{pkgnum}">Edit&nbsp;dates</A>!;
+}
+
+sub pkg_customize_link {
+  my $pkg = shift or return '';
+  return qq!<A HREF="${p}edit/part_pkg.cgi?keywords=$custnum;clone=$pkg->{pkgpart};pkgnum=$pkg->{pkgnum}">Customize</A>!;
+}
+
+%>
+
diff --git a/httemplate/view/cust_pkg.cgi b/httemplate/view/cust_pkg.cgi
new file mode 100755 (executable)
index 0000000..5f0e6bf
--- /dev/null
@@ -0,0 +1,164 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my %uiview = ();
+my %uiadd = ();
+foreach my $part_svc ( qsearch('part_svc',{}) ) {
+  $uiview{$part_svc->svcpart} = popurl(2). "view/". $part_svc->svcdb . ".cgi";
+  $uiadd{$part_svc->svcpart}= popurl(2). "edit/". $part_svc->svcdb . ".cgi";
+}
+
+my ($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $pkgnum = $1;
+
+#get package record
+my $cust_pkg = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+die "No package!" unless $cust_pkg;
+my $part_pkg = qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->getfield('pkgpart')});
+
+my $custnum = $cust_pkg->getfield('custnum');
+print header('Package View', menubar(
+  "View this customer (#$custnum)" => popurl(2). "view/cust_main.cgi?$custnum",
+  'Main Menu' => popurl(2)
+));
+
+#print info
+my ($susp,$cancel,$expire)=(
+  $cust_pkg->getfield('susp'),
+  $cust_pkg->getfield('cancel'),
+  $cust_pkg->getfield('expire'),
+);
+my($pkg,$comment)=($part_pkg->getfield('pkg'),$part_pkg->getfield('comment'));
+my($setup,$bill)=($cust_pkg->getfield('setup'),$cust_pkg->getfield('bill'));
+my $otaker = $cust_pkg->getfield('otaker');
+
+print <<END;
+<SCRIPT>
+function areyousure(href) {
+    if (confirm("Permanently delete included services and cancel this package?") == true)
+        window.location.href = href;
+}
+</SCRIPT>
+END
+
+print "Package information";
+print ' (<A HREF="'. popurl(2). 'misc/unsusp_pkg.cgi?'. $pkgnum.
+      '">unsuspend</A>)'
+  if ( $susp && ! $cancel );
+
+print ' (<A HREF="'. popurl(2). 'misc/susp_pkg.cgi?'. $pkgnum.
+      '">suspend</A>)'
+  unless ( $susp || $cancel );
+
+print ' (<A HREF="javascript:areyousure(\''. popurl(2). 'misc/cancel_pkg.cgi?'.
+      $pkgnum.  '\')">cancel</A>)'
+  unless $cancel;
+
+print ' (<A HREF="'. popurl(2). 'edit/REAL_cust_pkg.cgi?'. $pkgnum.
+      '">edit dates</A>)';
+
+print &ntable("#cccccc"), '<TR><TD>', &ntable("#cccccc",2),
+      '<TR><TD ALIGN="right">Package number</TD><TD BGCOLOR="#ffffff">',
+      $pkgnum, '</TD></TR>',
+      '<TR><TD ALIGN="right">Package</TD><TD BGCOLOR="#ffffff">',
+      $pkg,  '</TD></TR>',
+      '<TR><TD ALIGN="right">Comment</TD><TD BGCOLOR="#ffffff">',
+      $comment,  '</TD></TR>',
+      '<TR><TD ALIGN="right">Setup date</TD><TD BGCOLOR="#ffffff">',
+      ( $setup ? time2str("%D",$setup) : "(Not setup)" ), '</TD></TR>';
+
+print '<TR><TD ALIGN="right">Last bill date</TD><TD BGCOLOR="#ffffff">',
+      ( $cust_pkg->get('last_bill') ? time2str("%D",$cust_pkg->get('last_bill')) : "&nbsp;" ),
+      '</TD></TR>'
+  if $cust_pkg->dbdef_table->column('last_bill');
+
+print '<TR><TD ALIGN="right">Next bill date</TD><TD BGCOLOR="#ffffff">',
+      ( $bill ? time2str("%D",$bill) : "&nbsp;" ), '</TD></TR>';
+      
+print '<TR><TD ALIGN="right">Suspension date</TD><TD BGCOLOR="#ffffff">',
+       time2str("%D",$susp), '</TD></TR>' if $susp;
+print '<TR><TD ALIGN="right">Expiration date</TD><TD BGCOLOR="#ffffff">',
+       time2str("%D",$expire), '</TD></TR>' if $expire;
+print '<TR><TD ALIGN="right">Cancellation date</TD><TD BGCOLOR="#ffffff">',
+       time2str("%D",$cancel), '</TD></TR>' if $cancel;
+print  '<TR><TD ALIGN="right">Order taker</TD><TD BGCOLOR="#ffffff">',
+      $otaker,  '</TD></TR>',
+      '</TABLE></TD></TR></TABLE>';
+
+unless ($expire) {
+  print <<END;
+<FORM ACTION="../misc/expire_pkg.cgi" METHOD="post">
+<INPUT TYPE="hidden" NAME="pkgnum" VALUE="$pkgnum">
+Expire (date): <INPUT TYPE="text" NAME="date" VALUE="" >
+<INPUT TYPE="submit" VALUE="Cancel later">
+END
+}
+
+unless ($cancel) {
+
+  #services
+  print '<BR>Service Information', &table();
+
+  #list of services this pkgpart includes
+  my $pkg_svc;
+  my %pkg_svc = ();
+  foreach $pkg_svc ( qsearch('pkg_svc',{'pkgpart'=> $cust_pkg->pkgpart }) ) {
+    $pkg_svc{$pkg_svc->svcpart} = $pkg_svc->quantity if $pkg_svc->quantity;
+  }
+
+  #list of records from cust_svc
+  my $svcpart;
+  foreach $svcpart (sort {$a <=> $b} keys %pkg_svc) {
+
+    my($svc)=qsearchs('part_svc',{'svcpart'=>$svcpart})->getfield('svc');
+
+    my(@cust_svc)=qsearch('cust_svc',{'pkgnum'=>$pkgnum, 
+                                      'svcpart'=>$svcpart,
+                                     });
+
+    my($enum);
+    for $enum ( 1 .. $pkg_svc{$svcpart} ) {
+
+      my($cust_svc);
+      if ( $cust_svc=shift @cust_svc ) {
+        my($svcnum)=$cust_svc->svcnum;
+        my($label, $value, $svcdb) = $cust_svc->label;
+        print <<END;
+<TR><TD><A HREF="$uiview{$svcpart}?$svcnum">(View/Edit) $svc: $value<A></TD></TR>
+END
+      } else {
+        print qq!<TR><TD>!.
+              qq!<A HREF="$uiadd{$svcpart}?pkgnum$pkgnum-svcpart$svcpart">!.
+              qq!(Provision) $svc</A>!;
+
+        print qq! or <A HREF="../misc/link.cgi?pkgnum$pkgnum-svcpart$svcpart">!.
+              qq!(Link to legacy) $svc</A>!
+          if $conf->exists('legacy_link');
+
+        print '</TD></TR>';
+      }
+
+    }
+    warn "WARNING: Leftover services pkgnum $pkgnum!" if @cust_svc;; 
+  }
+
+  print "</TABLE><FONT SIZE=-1>",
+        "Choose (View/Edit) to view or edit an existing service<BR>",
+        "Choose (Provision) to setup a new service<BR>";
+
+  print "Choose (Link to legacy) to link to a legacy (pre-Freeside) service"
+    if $conf->exists('legacy_link');
+
+  print "</FONT>";
+}
+
+#formatting
+print <<END;
+  </BODY>
+</HTML>
+END
+
+%>
diff --git a/httemplate/view/svc_acct.cgi b/httemplate/view/svc_acct.cgi
new file mode 100755 (executable)
index 0000000..599c1d8
--- /dev/null
@@ -0,0 +1,203 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$svcnum});
+die "Unknown svcnum" unless $svc_acct;
+
+#false laziness w/all svc_*.cgi
+my $cust_svc = qsearchs( 'cust_svc' , { 'svcnum' => $svcnum } );
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+  $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } );
+  $custnum = $cust_pkg->custnum;
+} else {
+  $cust_pkg = '';
+  $custnum = '';
+}
+#eofalse
+
+my $part_svc = qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } );
+die "Unknown svcpart" unless $part_svc;
+
+my $domain;
+if ( $svc_acct->domsvc ) {
+  my $svc_domain = qsearchs('svc_domain', { 'svcnum' => $svc_acct->domsvc } );
+  die "Unknown domain" unless $svc_domain;
+  $domain = $svc_domain->domain;
+} else {
+  die "No svc_domain.svcnum record for svc_acct.domsvc: ". $cust_svc->domsvc;
+}
+
+%>
+
+<SCRIPT>
+function areyousure(href) {
+    if (confirm("Permanently delete this account?") == true)
+        window.location.href = href;
+}
+</SCRIPT>
+
+<%= header('Account View', menubar(
+  ( ( $pkgnum || $custnum )
+    ? ( "View this package (#$pkgnum)" => "${p}view/cust_pkg.cgi?$pkgnum",
+        "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+      )
+    : ( "Cancel this (unaudited) account" =>
+          "javascript:areyousure(\'${p}misc/cancel-unaudited.cgi?$svcnum\')" )
+  ),
+  "Main menu" => $p,
+)) %>
+
+<%
+
+#if ( $cust_pkg && $cust_pkg->part_pkg->plan eq 'sqlradacct_hour' ) {
+if ( $part_svc->part_export('sqlradius') ) {
+
+  my $last_bill;
+  my %plandata;
+  if ( $cust_pkg ) {
+    #false laziness w/httemplate/edit/part_pkg... this stuff doesn't really
+    #belong in plan data
+    %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); }
+                    split("\n", $cust_pkg->part_pkg->plandata );
+
+    $last_bill = $cust_pkg->last_bill;
+  } else {
+    $last_bill = 0;
+    %plandata = ();
+  }
+
+  my $seconds = $svc_acct->seconds_since_sqlradacct( $last_bill, time );
+  my $h = int($seconds/3600);
+  my $m = int( ($seconds%3600) / 60 );
+  my $s = $seconds%60;
+
+  my $input = $svc_acct->attribute_since_sqlradacct(
+    $last_bill, time, 'AcctInputOctets'
+  ) / 1048576;
+  my $output = $svc_acct->attribute_since_sqlradacct(
+    $last_bill, time, 'AcctOutputOctets'
+  ) / 1048576;
+
+  if ( $seconds ) {
+    print "Online <B>$h</B>h <B>$m</B>m <B>$s</B>s";
+  } else {
+    print 'Has not logged on';
+  }
+
+  if ( $cust_pkg ) {
+    print ' since last bill ('. time2str("%C", $last_bill). ') - '. 
+          $plandata{recur_included_hours}. ' total hours in plan<BR>';
+  } else {
+    print ' (no billing cycle available for unaudited account)<BR>';
+  }
+
+  print 'Input: <B>'. sprintf("%.3f", $input). '</B> megabytes<BR>';
+  print 'Output: <B>'. sprintf("%.3f", $output). '</B> megabytes<BR>';
+
+  print '<BR>';
+
+}
+
+#print qq!<BR><A HREF="../misc/sendconfig.cgi?$svcnum">Send account information</A>!;
+
+print qq!<A HREF="${p}edit/svc_acct.cgi?$svcnum">Edit this information</A><BR>!.
+      &ntable("#cccccc"). '<TR><TD>'. &ntable("#cccccc",2).
+      "<TR><TD ALIGN=\"right\">Service number</TD>".
+        "<TD BGCOLOR=\"#ffffff\">$svcnum</TD></TR>".
+      "<TR><TD ALIGN=\"right\">Service</TD>".
+        "<TD BGCOLOR=\"#ffffff\">". $part_svc->svc. "</TD></TR>".
+      "<TR><TD ALIGN=\"right\">Username</TD>".
+        "<TD BGCOLOR=\"#ffffff\">". $svc_acct->username. "</TD></TR>"
+;
+
+print "<TR><TD ALIGN=\"right\">Domain</TD>".
+        "<TD BGCOLOR=\"#ffffff\">". $domain, "</TD></TR>";
+
+print "<TR><TD ALIGN=\"right\">Password</TD><TD BGCOLOR=\"#ffffff\">";
+my $password = $svc_acct->_password;
+if ( $password =~ /^\*\w+\* (.*)$/ ) {
+  $password = $1;
+  print "<I>(login disabled)</I> ";
+}
+if ( $conf->exists('showpasswords') ) {
+  print '<PRE>'. encode_entities($password). '</PRE>';
+} else {
+  print "<I>(hidden)</I>";
+}
+print "</TR></TD>";
+$password = '';
+
+if ( $conf->exists('security_phrase') ) {
+  my $sec_phrase = $svc_acct->sec_phrase;
+  print '<TR><TD ALIGN="right">Security phrase</TD><TD BGCOLOR="#ffffff">'.
+        $svc_acct->sec_phrase. '</TD></TR>';
+}
+
+my $svc_acct_pop = $svc_acct->popnum
+                     ? qsearchs('svc_acct_pop',{'popnum'=>$svc_acct->popnum})
+                     : '';
+print "<TR><TD ALIGN=\"right\">Access number</TD>".
+      "<TD BGCOLOR=\"#ffffff\">". $svc_acct_pop->text. '</TD></TR>'
+  if $svc_acct_pop;
+
+if ($svc_acct->uid ne '') {
+  print "<TR><TD ALIGN=\"right\">Uid</TD>".
+          "<TD BGCOLOR=\"#ffffff\">". $svc_acct->uid. "</TD></TR>",
+        "<TR><TD ALIGN=\"right\">Gid</TD>".
+          "<TD BGCOLOR=\"#ffffff\">". $svc_acct->gid. "</TD></TR>",
+        "<TR><TD ALIGN=\"right\">GECOS</TD>".
+          "<TD BGCOLOR=\"#ffffff\">". $svc_acct->finger. "</TD></TR>",
+        "<TR><TD ALIGN=\"right\">Home directory</TD>".
+          "<TD BGCOLOR=\"#ffffff\">". $svc_acct->dir. "</TD></TR>",
+        "<TR><TD ALIGN=\"right\">Shell</TD>".
+          "<TD BGCOLOR=\"#ffffff\">". $svc_acct->shell. "</TD></TR>",
+        "<TR><TD ALIGN=\"right\">Quota</TD>".
+          "<TD BGCOLOR=\"#ffffff\">". $svc_acct->quota. "</TD></TR>"
+  ;
+} else {
+  print "<TR><TH COLSPAN=2>(No shell account)</TH></TR>";
+}
+
+if ($svc_acct->slipip) {
+  print "<TR><TD ALIGN=\"right\">IP address</TD><TD BGCOLOR=\"#ffffff\">".
+        ( ( $svc_acct->slipip eq "0.0.0.0" || $svc_acct->slipip eq '0e0' )
+          ? "<I>(Dynamic)</I>"
+          : $svc_acct->slipip
+        ). "</TD>";
+  my($attribute);
+  foreach $attribute ( grep /^radius_/, fields('svc_acct') ) {
+    #warn $attribute;
+    $attribute =~ /^radius_(.*)$/;
+    my $pattribute = $FS::raddb::attrib{$1};
+    print "<TR><TD ALIGN=\"right\">Radius (reply) $pattribute</TD>".
+          "<TD BGCOLOR=\"#ffffff\">". $svc_acct->getfield($attribute).
+          "</TD></TR>";
+  }
+  foreach $attribute ( grep /^rc_/, fields('svc_acct') ) {
+    #warn $attribute;
+    $attribute =~ /^rc_(.*)$/;
+    my $pattribute = $FS::raddb::attrib{$1};
+    print "<TR><TD ALIGN=\"right\">Radius (check) $pattribute: </TD>".
+          "<TD BGCOLOR=\"#ffffff\">". $svc_acct->getfield($attribute).
+          "</TD></TR>";
+  }
+} else {
+  print "<TR><TH COLSPAN=2>(No SLIP/PPP account)</TH></TR>";
+}
+
+print '<TR><TD ALIGN="right">RADIUS groups</TD><TD BGCOLOR="#ffffff">'.
+      join('<BR>', $svc_acct->radius_groups). '</TD></TR>';
+
+print '</TABLE></TD></TR></TABLE><BR><BR>';
+
+print join("\n", $conf->config('svc_acct-notes') ). '<BR><BR>'.
+      joblisting({'svcnum'=>$svcnum}, 1). '</BODY></HTML>';
+
+%>
diff --git a/httemplate/view/svc_broadband.cgi b/httemplate/view/svc_broadband.cgi
new file mode 100644 (file)
index 0000000..164b5b2
--- /dev/null
@@ -0,0 +1,91 @@
+<!-- mason kludge -->
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_broadband = qsearchs( 'svc_broadband', { 'svcnum' => $svcnum } )
+  or die "svc_broadband: Unknown svcnum $svcnum";
+
+#false laziness w/all svc_*.cgi
+my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $svcnum } );
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+  $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } );
+  $custnum = $cust_pkg->custnum;
+} else {
+  $cust_pkg = '';
+  $custnum = '';
+}
+#eofalse
+
+my $router = $svc_broadband->addr_block->router;
+
+if (not $router) { die "Could not lookup router for svc_broadband (svcnum $svcnum)" };
+
+my (
+     $routername,
+     $routernum,
+     $speed_down,
+     $speed_up,
+     $ip_addr
+   ) = (
+     $router->getfield('routername'),
+     $router->getfield('routernum'),
+     $svc_broadband->getfield('speed_down'),
+     $svc_broadband->getfield('speed_up'),
+     $svc_broadband->getfield('ip_addr')
+   );
+
+
+
+print header('Broadband Service View', menubar(
+  ( ( $custnum )
+    ? ( "View this package (#$pkgnum)" => "${p}view/cust_pkg.cgi?$pkgnum",
+        "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+      )                                                                       
+    : ( "Cancel this (unaudited) website" =>
+          "${p}misc/cancel-unaudited.cgi?$svcnum" )
+  ),
+  "Main menu" => $p,
+)).
+      qq!<A HREF="${p}edit/svc_broadband.cgi?$svcnum">Edit this information</A><BR>!.
+      ntable("#cccccc"). '<TR><TD>'. ntable("#cccccc",2).
+      qq!<TR><TD ALIGN="right">Service number</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$svcnum</TD></TR>!.
+      qq!<TR><TD ALIGN="right">Router</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$routernum: $routername</TD></TR>!.
+      qq!<TR><TD ALIGN="right">Download Speed</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$speed_down</TD></TR>!.
+      qq!<TR><TD ALIGN="right">Upload Speed</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$speed_up</TD></TR>!.
+      qq!<TR><TD ALIGN="right">IP Address</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$ip_addr</TD></TR>!.
+      '</TD></TR><TR ROWSPAN="1"><TD></TD></TR>';
+
+
+#  foreach my $sb_field 
+#      ( qsearch('sb_field', { svcnum => $svcnum }) ) {
+#    my $part_sb_field = qsearchs('part_sb_field',
+#                         { sbfieldpart => $sb_field->sbfieldpart });
+#    print q!<TR><TD ALIGN="right">! . $part_sb_field->name . 
+#          q!</TD><TD BGCOLOR="#ffffff">! . $sb_field->value . 
+#          q!</TD></TR>!;
+#  }
+#  print '</TABLE>';
+
+
+  my $sb_field = $svc_broadband->sb_field_hashref;
+  foreach (sort { $a cmp $b } keys(%{$sb_field})) {
+    print q!<TR><TD ALIGN="right">! . $_ . 
+          q!</TD><TD BGCOLOR="#ffffff">! . $sb_field->{$_} .
+          q!</TD></TR>!;
+  }
+  print '</TABLE>';
+
+
+print '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
+      '</BODY></HTML>'
+;
+%>
diff --git a/httemplate/view/svc_domain.cgi b/httemplate/view/svc_domain.cgi
new file mode 100755 (executable)
index 0000000..fd017de
--- /dev/null
@@ -0,0 +1,106 @@
+<!-- mason kludge -->
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_domain = qsearchs('svc_domain',{'svcnum'=>$svcnum});
+die "Unknown svcnum" unless $svc_domain;
+
+my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$svcnum});
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+  $cust_pkg=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+  $custnum=$cust_pkg->getfield('custnum');
+} else {
+  $cust_pkg = '';
+  $custnum = '';
+}
+
+my $part_svc = qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } );
+die "Unknown svcpart" unless $part_svc;
+
+my $email = '';
+if ($svc_domain->catchall) {
+  my $svc_acct = qsearchs('svc_acct',{'svcnum'=> $svc_domain->catchall } );
+  die "Unknown svcpart" unless $svc_acct;
+  $email = $svc_acct->email;
+}
+
+my $domain = $svc_domain->domain;
+
+%>
+
+<%= header('Domain View', menubar(
+  ( ( $pkgnum || $custnum )
+    ? ( "View this package (#$pkgnum)" => "${p}view/cust_pkg.cgi?$pkgnum",
+        "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+      )
+    : ( "Cancel this (unaudited) domain" =>
+          "${p}misc/cancel-unaudited.cgi?$svcnum" )
+  ),
+  "Main menu" => $p,
+)) %>
+
+Service #<%= $svcnum %>
+<BR>Service: <B><%= $part_svc->svc %></B>
+<BR>Domain name: <B><%= $domain %></B>
+<BR>Catch all email <A HREF="<%= ${p} %>misc/catchall.cgi?<%= $svcnum %>">(change)</A>:
+<%= $email ? "<B>$email</B>" : "<I>(none)<I>" %>
+<BR><BR><A HREF="http://www.geektools.com/cgi-bin/proxy.cgi?query=<%=$domain%>;targetnic=auto">View whois information.</A>
+<BR><BR>
+<SCRIPT>
+  function areyousure(href) {
+    if ( confirm("Remove this record?") == true )
+      window.location.href = href;
+  }
+</SCRIPT>
+
+<% my @records; if ( @records = $svc_domain->domain_record ) { %>
+  <%= ntable("",2) %>
+  <tr><th>Zone</th><th>Type</th><th>Data</th></tr>
+
+  <% foreach my $domain_record ( @records ) {
+       my $type = $domain_record->rectype eq '_mstr'
+                    ? "(slave)"
+                    : $domain_record->recaf. ' '. $domain_record->rectype;
+  %>
+
+    <tr><td><%= $domain_record->reczone %></td>
+    <td><%= $type %></td>
+    <td><%= $domain_record->recdata %>
+
+    <% unless ( $domain_record->rectype eq 'SOA' ) { %>
+      (<A HREF="javascript:areyousure('<%=$p%>misc/delete-domain_record.cgi?<%=$domain_record->recnum%>')">delete</A>)
+    <% } %>
+    </td></tr>
+  <% } %>
+  </table>
+<% } %>
+
+<BR>
+<FORM METHOD="POST" ACTION="<%=$p%>edit/process/domain_record.cgi">
+<INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+<INPUT TYPE="text" NAME="reczone"> 
+<INPUT TYPE="hidden" NAME="recaf" VALUE="IN"> IN 
+ <SELECT NAME="rectype">
+<% foreach (qw( A NS CNAME MX PTR) ) { %>
+  <OPTION VALUE="<%=$_%>"><%=$_%></OPTION>
+<% } %>
+ </SELECT>
+<INPUT TYPE="text" NAME="recdata"> <INPUT TYPE="submit" VALUE="Add record">
+</FORM><BR><BR>or<BR><BR>
+<FORM METHOD="POST" ACTION="<%=$p%>edit/process/domain_record.cgi">
+<INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+
+<% if ( @records ) { %> Delete all records and <% } %>
+Slave from nameserver IP 
+<INPUT TYPE="hidden" NAME="svcnum" VALUE="<%=$svcnum%>">
+<INPUT TYPE="hidden" NAME="reczone" VALUE="@"> 
+<INPUT TYPE="hidden" NAME="recaf" VALUE="IN">
+<INPUT TYPE="hidden" NAME="rectype" VALUE="_mstr">
+<INPUT TYPE="text" NAME="recdata"> <INPUT TYPE="submit" VALUE="Slave domain">
+</FORM>
+<BR><BR><%= joblisting({'svcnum'=>$svcnum}, 1) %>
+</BODY></HTML>
diff --git a/httemplate/view/svc_forward.cgi b/httemplate/view/svc_forward.cgi
new file mode 100755 (executable)
index 0000000..c8d1d62
--- /dev/null
@@ -0,0 +1,69 @@
+<!-- mason kludge -->
+<%
+
+my $conf = new FS::Conf;
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_forward = qsearchs('svc_forward',{'svcnum'=>$svcnum});
+die "Unknown svcnum" unless $svc_forward;
+
+my $cust_svc = qsearchs('cust_svc',{'svcnum'=>$svcnum});
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+  $cust_pkg=qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
+  $custnum=$cust_pkg->getfield('custnum');
+} else {
+  $cust_pkg = '';
+  $custnum = '';
+}
+
+my $part_svc = qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } )
+  or die "Unkonwn svcpart";
+
+print header('Mail Forward View', menubar(
+  ( ( $pkgnum || $custnum )
+    ? ( "View this package (#$pkgnum)" => "${p}view/cust_pkg.cgi?$pkgnum",
+        "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+      )
+    : ( "Cancel this (unaudited) account" =>
+          "${p}misc/cancel-unaudited.cgi?$svcnum" )
+  ),
+  "Main menu" => $p,
+));
+
+my($srcsvc,$dstsvc,$dst) = (
+  $svc_forward->srcsvc,
+  $svc_forward->dstsvc,
+  $svc_forward->dst,
+);
+my $svc = $part_svc->svc;
+my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$srcsvc})
+  or die "Corrupted database: no svc_acct.svcnum matching srcsvc $srcsvc";
+my $source = $svc_acct->email;
+my $destination;
+if ($dstsvc) {
+  my $svc_acct = qsearchs('svc_acct',{'svcnum'=>$dstsvc})
+    or die "Corrupted database: no svc_acct.svcnum matching dstsvc $dstsvc";
+  $destination = $svc_acct->email;
+}else{
+  $destination = $dst;
+}
+
+print qq!<A HREF="${p}edit/svc_forward.cgi?$svcnum">Edit this information</A>!.
+      ntable("#cccccc",2).
+      '<TR><TD ALIGN="right">Service number</TD>'.
+        qq!<TD BGCOLOR="#ffffff">$svcnum</TD></TR>!.
+      '<TR><TD ALIGN="right">Service</TD>'.
+        qq!<TD BGCOLOR="#ffffff">$svc</TD></TR>!.
+      qq!<TR><TD ALIGN="right">Email to</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$source</TD></TR>!.
+      qq!<TR><TD ALIGN="right">Forwards to </TD>!.
+        qq!<TD BGCOLOR="#ffffff">$destination</TD></TR></TABLE>!.
+      '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
+      '</BODY></HTML>'
+;
+
+%>
diff --git a/httemplate/view/svc_www.cgi b/httemplate/view/svc_www.cgi
new file mode 100644 (file)
index 0000000..4426144
--- /dev/null
@@ -0,0 +1,55 @@
+<!-- mason kludge -->
+<%
+
+my($query) = $cgi->keywords;
+$query =~ /^(\d+)$/;
+my $svcnum = $1;
+my $svc_www = qsearchs( 'svc_www', { 'svcnum' => $svcnum } )
+  or die "svc_www: Unknown svcnum $svcnum";
+
+#false laziness w/all svc_*.cgi
+my $cust_svc = qsearchs( 'cust_svc', { 'svcnum' => $svcnum } );
+my $pkgnum = $cust_svc->getfield('pkgnum');
+my($cust_pkg, $custnum);
+if ($pkgnum) {
+  $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $pkgnum } );
+  $custnum = $cust_pkg->custnum;
+} else {
+  $cust_pkg = '';
+  $custnum = '';
+}
+#eofalse
+
+my $usersvc = $svc_www->usersvc;
+my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $usersvc } )
+  or die "svc_www: Unknown usersvc $usersvc";
+my $email = $svc_acct->email;
+
+my $domain_record = qsearchs('domain_record', { 'recnum' => $svc_www->recnum } )
+  or die "svc_www: Unknown recnum ". $svc_www->recnum;
+
+my $www = $domain_record->zone;
+
+print header('Website View', menubar(
+  ( ( $custnum )
+    ? ( "View this package (#$pkgnum)" => "${p}view/cust_pkg.cgi?$pkgnum",
+        "View this customer (#$custnum)" => "${p}view/cust_main.cgi?$custnum",
+      )                                                                       
+    : ( "Cancel this (unaudited) website" =>
+          "${p}misc/cancel-unaudited.cgi?$svcnum" )
+  ),
+  "Main menu" => $p,
+)).
+      qq!<A HREF="${p}edit/svc_www.cgi?$svcnum">Edit this information</A><BR>!.
+      ntable("#cccccc"). '<TR><TD>'. ntable("#cccccc",2).
+      qq!<TR><TD ALIGN="right">Service number</TD>!.
+        qq!<TD BGCOLOR="#ffffff">$svcnum</TD></TR>!.
+      qq!<TR><TD ALIGN="right">Website name</TD>!.
+        qq!<TD BGCOLOR="#ffffff"><A HREF="http://$www">$www<A></TD></TR>!.
+      qq!<TR><TD ALIGN="right">Account</TD>!.
+        qq!<TD BGCOLOR="#ffffff"><A HREF="${p}view/svc_acct.cgi?$usersvc">$email</A></TD></TR>!.
+      '</TABLE></TD></TR></TABLE>'.
+      '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
+      '</BODY></HTML>'
+;
+%>
diff --git a/init.d/freeside-init b/init.d/freeside-init
new file mode 100644 (file)
index 0000000..68cba8d
--- /dev/null
@@ -0,0 +1,70 @@
+#! /bin/sh
+#
+# chkconfig: 345 86 16
+# description: Freeside daemons
+
+QUEUED_USER=%%%QUEUED_USER%%%
+
+FREESIDE_PATH="%%%FREESIDE_PATH%%%"
+
+PASSWD_USER=%%%PASSWD_USER%%%
+PASSWD_MACHINE=%%%PASSWD_MACHINE%%%
+
+SIGNUP_USER=%%%SIGNUP_USER%%%
+SIGNUP_MACHINE=%%%SIGNUP_MACHINE%%%
+SIGNUP_AGENTNUM=%%%SIGNUP_AGENTNUM%%%
+SIGNUP_REFNUM=%%%SIGNUP_REFNUM%%%
+
+SELFSERVICE_USER=%%%SELFSERVICE_USER%%%
+SELFSERVICE_MACHINE=%%%SELFSERVICE_MACHINE%%%
+
+case "$1" in
+  start)
+        # Start daemons.
+        echo -n "Starting freeside-queued: "
+        freeside-queued $QUEUED_USER
+        echo "done."
+
+        echo -n "Starting fs_passwd_server: "
+        su freeside -c "$FREESIDE_PATH/fs_passwd/fs_passwd_server $PASSWD_USER $PASSWD_MACHINE" &
+        echo "done."
+
+        echo -n "Starting fs_signup_server: "
+        su freeside -c "$FREESIDE_PATH/fs_signup/fs_signup_server $SIGNUP_USER $SIGNUP_MACHINE $SIGNUP_AGENTNUM $SIGNUP_REFNUM" &
+        echo "done."
+
+        echo -n "Starting freeside-selfservice-server: "
+        freeside-selfservice-server $SELFSERVICE_USER $SELFSERVICE_MACHINE
+        echo "done."
+
+        ;;
+  stop)
+        # Stop daemons.
+        echo -n "Stopping freeside-queued: "
+        kill `cat /var/run/freeside-queued.pid`
+        echo "done."
+
+        echo -n "Stopping fs_passwd_server: "
+        killall fs_passwd_server
+        echo "done."
+
+        echo -n "Stopping fs_signup_server: "
+        killall fs_signup_server
+        echo "done."
+
+        echo -n "Stopping freeside-selfservice-server: "
+        kill `cat /var/run/freeside-selfservice-server.pid`
+        echo "done."
+        ;;
+
+  restart)
+        $0 stop
+        $0 start
+        ;;
+  *)
+        echo "Usage: freeside {start|stop|restart}"
+        exit 1
+esac
+
+exit 0
+
diff --git a/install/freebsd/INSTALL b/install/freebsd/INSTALL
new file mode 100755 (executable)
index 0000000..53fc613
--- /dev/null
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+( cd /usr/ports/sysutils/portupgrade
+  make install
+)
+
+pkgdb -u
+
+portinstall -PR cvsup-without-gui
+
+cp /usr/share/examples/cvsup/ports-supfile /root
+perl -pi -e 's/CHANGE_THIS/cvsup1/;' /root/ports-supfile
+cvsup /root/ports-supfile
+
+for port in `grep -v '^ *#' ports`; do
+  #cd /usr/ports/$port
+  #make install || exit
+  portinstall -P -R $port || exit
+done
+
+for a in Net::SSH DBIx::DBSchema HTML::Widgets::SelectLayers Time::Duration Business::CreditCard; do perl -MCPAN -e"install $a"; done
+
+su -l pgsql -c initdb
+
+/usr/local/etc/rc.d/010.pgsql.sh start
+
+pw user add freeside -m
+
+su -l pgsql -c 'createuser -P freeside'
+
+su -l freeside -c 'createdb freeside'
+
+#?
+cd ../..
+make install-perl-modules
+make create-config
+make deploy
+
+#edit apache config, etc.
+
diff --git a/install/freebsd/ports b/install/freebsd/ports
new file mode 100644 (file)
index 0000000..1e04a42
--- /dev/null
@@ -0,0 +1,43 @@
+shells/zsh
+misc/screen
+ftp/lftp
+www/apache13-modssl
+www/mod_perl
+net/rsync
+databases/postgresql7
+misc/p5-Array-PrintCols
+devel/p5-Term-Query
+converters/p5-MIME-Base64
+security/p5-Digest-MD5
+security/p5-MD5
+net/p5-URI
+www/p5-HTML-Tagset
+www/p5-HTML-Parser
+net/p5-Net
+misc/p5-Locale-Codes
+net/p5-Net-Whois
+www/p5-libwww
+     #misc/p5-Business-CreditCard
+devel/p5-Data-ShowTable
+mail/p5-Mail-Tools
+devel/p5-TimeDate
+devel/p5-Date-Manip
+misc/p5-File-CounterFile
+devel/p5-FreezeThaw
+devel/p5-String-Approx
+textproc/p5-Text-Template
+databases/p5-DBI
+databases/p5-DBD-Pg
+#databases/p5-DBD-mysql
+databases/p5-DBIx-DataSource
+    #database/p5-DBIx-DBSchema
+    #net/p5-Net-SSH
+textproc/p5-String-ShellQuote
+net/p5-Net-SCP
+www/p5-Apache-ASP
+    #www/p5-HTML-Mason
+devel/p5-Tie-IxHash
+    #devel/p5-Time-Duration
+    #www/p5-HTML-Widgets-SelectLayers
+devel/p5-Storable
+www/p5-Apache-DBI
diff --git a/install/redhat/7.3/INSTALL b/install/redhat/7.3/INSTALL
new file mode 100644 (file)
index 0000000..4c07f88
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+wget ftp://apt-rpm.tuxfamily.org/apt/redhat/7.3/en/i386/RPMS.extra/apt-*i386.rpm
+rpm -i apt*i386.rpm
+cp sources.list /etc/apt/
+apt-get update; apt-get update
+apt-get install apache mod_ssl mod_perl perl-CGI perl-CPAN perl-DBD-MySQL perl-DBD-Pg perl-DBI perl-DateManip perl-Digest-MD5 perl-HTML-Parser perl-HTML-Tagset perl-MIME-Base64 perl-Storable perl-TimeDate perl-URI perl-libnet perl-libwww-perl perl-suidperl rsync postgresql postgresql-docs postgresql-libs postgresql-server screen zsh lftp cvs #openssh
+
+perl -MCPAN -e"install Locale::Country, Net::Whois, Business::CreditCard, \
+                       Mail::Internet, File::CounterFile, FreezeThaw, \
+                       String::Approx, Text::Template, DBIx::DataSource, \
+                       DBIx::DBSchema, Net::SSH, String::ShellQuote, \
+                       Net::SCP, Apache::ASP, Tie::IxHash, Time::Duration, \
+                       HTML::Widgets::SelectLayers,  Apache::DBI"
diff --git a/install/redhat/7.3/sources.list b/install/redhat/7.3/sources.list
new file mode 100644 (file)
index 0000000..9a9ad5c
--- /dev/null
@@ -0,0 +1,2 @@
+rpm ftp://apt-rpm.tuxfamily.org/apt redhat/7.3/en/i386 os updates extra
+rpm-src ftp://apt-rpm.tuxfamily.org/apt redhat/7.3/en/i386 os updates extra
diff --git a/rt/COPYING b/rt/COPYING
new file mode 100755 (executable)
index 0000000..e77696a
--- /dev/null
@@ -0,0 +1,339 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                          675 Mass Ave, Cambridge, MA 02139, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/rt/ChangeLog b/rt/ChangeLog
new file mode 100644 (file)
index 0000000..549a5ca
--- /dev/null
@@ -0,0 +1,16016 @@
+2002-07-19 22:47  jesse
+
+       * README:
+
+       Fixed the readme about fastcgi
+       
+2002-07-19 22:42  jesse
+
+       * Makefile:
+
+       Bumped the version to 2.0.14
+       
+2002-07-19 01:22  jesse
+
+       * Makefile, bin/rt, webrt/Search/Bulk.html:
+
+       RT-Ticket: 1547
+       
+       Bumping the version to 2.0.14-pre4
+       Fixing a typo that pdh caught in Tickets/Bulk.html
+       
+2002-07-13 00:22  jesse
+
+       * Makefile:
+
+       Bumped to 2.0.14-pre3
+       
+2002-07-13 00:19  jesse
+
+       * bin/webmux.pl:
+
+       modperl handler now speaks Mason 1.11 properly.
+       
+2002-07-13 00:00  jesse
+
+       * etc/config.pm:
+
+       Shifted some config defaults to make things easier for newbie users
+       
+2002-07-12 13:31  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Now we properly strip long pathnames from attachments uploaded from windows boxes.
+       
+2002-07-11 01:41  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Fixing a typo in the mason 11 handler
+       
+2002-07-10 14:41  jesse
+
+       * Makefile:
+
+       Bumping the version to 2.0.14-pre2
+       
+2002-07-10 14:36  jesse
+
+       * lib/RT/User.pm:
+
+       [no log message]
+       
+2002-07-10 14:35  jesse
+
+       * lib/RT/: Queue.pm, Ticket.pm:
+
+       RT-Ticket: 1418
+       RT-Status: resolved
+       
+       minor perldoc cleanups from pdh@snapgear
+       
+2002-07-10 14:32  jesse
+
+       * bin/rt-mailgate, lib/RT/Action/SendEmail.pm:
+
+       RT-Ticket: 1425
+       RT-Status: resolved
+       RT-Milestone: 2.0.x
+       RT-Subsystem: Mail Sending
+       
+       Fixes to create more proper message ids.
+       
+2002-07-10 14:28  jesse
+
+       * lib/RT/Ticket.pm:
+
+       RT-Ticket: 1431
+       RT-Status: resolved
+       
+       Importing tickets now lets you set the "Resolved" date
+       
+2002-07-10 14:17  jesse
+
+       * lib/RT/Tickets.pm:
+
+       RT-Ticket: 1434
+       RT-Status: resolved
+       RT-Subsystem: Core
+       RT-Milestone: 2.0.x
+       
+       RT is now smarter about letting you do "or" searches on single-value
+       keyword selections. Thanks to sam hartman.
+       
+2002-07-10 14:15  jesse
+
+       * webrt/Ticket/: Display.html, ModifyAll.html:
+
+       RT-Ticket: 1433
+       RT-Status: resolved
+       RT-Milestone: 2.0.x
+       RT-Subsystem: HTML::Mason frontend
+       RT-Severity: Nice to have
+       RT-Broken In: 2.0.13
+       RT-Broken In: 2.0.12
+       RT-Broken In: 2.0.11
+       RT-Broken In: 2.0.10
+       RT-Broken In: 2.0.0
+       RT-Broken In: 2.0.1
+       RT-Broken In: 2.0.2
+       RT-Broken In: 2.0.3
+       RT-Broken In: 2.0.4
+       RT-Broken In: 2.0.5
+       RT-Broken In: 2.0.6
+       RT-Broken In: 2.0.7
+       RT-Broken In: 2.0.8
+       RT-Broken In: 2.0.9
+       
+       Some signatures weren't setting off the "don't record comments if the update
+       is only a signature" code. Fixed.
+       
+2002-07-10 13:59  jesse
+
+       * lib/RT/Transaction.pm:
+
+       RT-Ticket: 1501
+       RT-Status: resolved
+       
+       Fixing docs in Transaction.pm's Message method
+       
+2002-07-10 13:19  jesse
+
+       * bin/rt:
+
+       RT-Ticket: 1528
+       RT-Milestone: 2.0.x
+       RT-Subsystem: CLI
+       RT-Severity: Normal
+       RT-Status: resolved
+       
+       Fixed docs for cli to say that --limit-status=dead isn't a valid option
+       
+2002-06-26 15:22  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       support for mason 1.1
+       
+2002-06-26 15:19  jesse
+
+       * Makefile, bin/mason_handler.fcgi, bin/webmux.pl,
+       webrt/Admin/Queues/GroupRights.html,
+       webrt/Admin/Queues/UserRights.html, webrt/Elements/ListActions,
+       webrt/Ticket/Elements/ShowBasics, webrt/Ticket/Elements/ToolBar:
+
+       Adding support for mason 1.10
+       
+2002-06-26 15:15  jesse
+
+       * lib/RT/Action/Notify.pm:
+
+       Fixed notify cc behavior
+       
+2002-06-26 15:13  jesse
+
+       * lib/RT/Action/SendEmail.pm:
+
+       Fixed pseudo-list syntax in To: lines
+       
+2002-06-26 15:09  jesse
+
+       * webrt/Search/Bulk.html:
+
+       Added support for bulk comment/reply
+       
+2002-05-03 02:07  jesse
+
+       * lib/RT/Ticket.pm:
+
+       RT-Ticket: 1369
+       RT-Status: resolved
+       
+       When a ticket has another merged into it, it now has its "LastUpdated" date
+       updated
+       
+2002-05-03 01:58  jesse
+
+       * lib/RT/Ticket.pm:
+
+       RT-Ticket: 1412
+       RT-status: resolved
+       
+       Fixed a docs bug in Ticket->Import which didn't make it clear that Import
+       took an "Id" parameter and "Create" didn't.
+       
+2002-05-03 01:54  jesse
+
+       * webrt/autohandler:
+
+       rt-ticket: 1410
+       rt-status: resolved
+       
+       Applied a patch from rich lafferty which prevented NoAuth from not
+       requiring authentication on some fastcgi setups.
+       
+       A similar bug bit SelfService.
+       
+       This commit fixes that one too.
+       
+2002-05-03 01:51  jesse
+
+       * Makefile:
+
+       RT-Ticket: 1272
+       rt-status: resolved
+       
+       Applied a patch from Ilya Martynov which allows make insert to work
+       in a scenario where DESTDIR is being set to something funny This may be necessary when installing into AFS
+       
+2002-05-03 01:36  jesse
+
+       * lib/RT/Action/SendEmail.pm:
+
+       RT-Ticket: 1367
+       RT-Status: resolved
+       
+       Added a check which only sets precedence to bulk if it's not already set,
+       say by a template.
+       
+2002-05-03 01:30  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       RT-Ticket: 1370
+       RT-Status: resolved
+       
+       From: Jason Edgecombe <jedgecombe@carolina.rr.com>
+       To: rt-devel@lists.fsck.com
+       Subject: [rt-devel] An oversite in Interface/Email.pm
+       
+       Hi,
+       
+          I found an problem in Interface/Email.pm when I was modifying
+          enhanced mailgate. I have attached a diff of the modifications.
+       
+          In the function MailError, it assumes MIMIEOBJ is defined. I added a
+          simple "if" test to only run $MIMEOBJ->sync_headers if $MIMEOBJ is defined.
+       
+2002-05-03 01:24  jesse
+
+       * lib/RT/Action/SendEmail.pm:
+
+       RT-Ticket: 1348
+       RT-Status: resolved
+       
+       Fixed a bug in mail sending that improperly quoted the usernames of users
+       who had " in their names
+       
+2002-04-29 00:39  jesse
+
+       * lib/RT.pm:
+
+       Fixed a tiny typo
+       
+2002-04-28 23:46  jesse
+
+       * lib/RT.pm:
+
+       Fixed up the rt log messages.
+       
+2002-04-21 02:14  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Fixed a couple bugs in setting owners that were only revealed by the poor data
+       validation on rt.cpan.org
+       
+2002-04-21 02:06  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Fixed a couple bugs in setting owners that were only revealed by the poor data
+       validation on rt.cpan.org
+       
+2002-04-19 00:32  jesse
+
+       * Makefile:
+
+       Adding a new Makefile target to ease packaging.
+       
+2002-04-18 12:47  jesse
+
+       * lib/RT/GroupMember.pm:
+
+       RT-Ticket: 1385
+       RT-Broken-In: 2.0.0
+       RT-Broken-In: 2.0.1
+       RT-Broken-In: 2.0.2
+       RT-Broken-In: 2.0.3
+       RT-Broken-In: 2.0.4
+       RT-Broken-In: 2.0.5
+       RT-Broken-In: 2.0.6
+       RT-Broken-In: 2.0.7
+       RT-Broken-In: 2.0.8
+       RT-Broken-In: 2.0.9
+       RT-Broken-In: 2.0.10
+       RT-Broken-In: 2.0.11
+       RT-Broken-In: 2.0.12
+       RT-Broken-In: 2.0.13
+       RT-Status: resolved
+       
+       GroupMemmber was looking for the "ModifyGroups" right, when it should have
+       been looking for the "AdminGroups" right
+       
+2002-04-05 10:24  jesse
+
+       * webrt/Ticket/Update.html:
+
+       RT-Ticket: 1330
+       RT-Status: Resolved
+       
+       Fixed an html escaping bug in Ticket/Update.html
+       
+2002-03-27 22:59  jesse
+
+       * Makefile, lib/RT/User.pm:
+
+       Fixed a CRITICAL security bug that allowed remote administrative access
+       to RT without a password.  (Security advisory to follow)
+       
+2002-03-14 16:15  jesse
+
+       * Makefile:
+
+       Bumped the version to 2.0.12
+       
+2002-03-06 18:57  jesse
+
+       * Makefile:
+
+       Bumped the version to 2.0.12pre6
+       
+2002-03-06 18:57  jesse
+
+       * lib/RT/Transaction.pm:
+
+       Added an update that will make dates changed as parts of transaction updates show up in local time
+       
+2002-03-06 18:56  jesse
+
+       * webrt/Admin/Queues/Modify.html:
+
+       Added a note to the queue creation screen about defaults.
+       
+2002-03-01 01:41  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       This patch fixes a small problem with printing a message.It checks if themessage is true, not if it has length, so a message containing 0 will notbe printed.  From Blair Zajac <blair@orcaware.com>
+       
+2002-03-01 01:39  jesse
+
+       * bin/rt:
+
+       RT-Ticket:438
+       RT-Status: resolved
+       
+       Added brandon's patch to allow searching for tickets by keyword on the commandline
+       
+2002-02-28 02:03  jesse
+
+       * bin/rt:
+
+       RT-Ticket: 1258
+       RT-Milestone: 2.0.x
+       RT-Subsystem: CLI
+       RT-Severity: normal
+       RT-Broken in: 2.0-beta3
+       RT-Broken in: 2.0.1
+       RT-Broken in: 2.0.2
+       RT-Broken in: 2.0.3
+       RT-Broken in: 2.0.4
+       RT-Broken in: 2.0.5
+       RT-Broken in: 2.0.6
+       RT-Broken in: 2.0.7
+       RT-Broken in: 2.0.8
+       RT-Broken in: 2.0.9
+       RT-Broken in: 2.0.10
+       RT-Broken in: 2.0.11
+       
+       Removed a line that make --limit-priority and --limit-final-priority not
+       work
+       
+2002-02-28 01:49  jesse
+
+       * lib/RT/Action/: Autoreply.pm, SendEmail.pm:
+
+       RT-Ticket: 1196
+       RT-Status: resolved
+       
+2002-02-28 01:38  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       RT-Ticket: 1246
+       RT-Status: resolved
+       
+2002-02-28 01:38  jesse
+
+       * tools/testdeps:
+
+       Added a couple explicit dependencies to testdeps that should have been there
+       forever ago. you'll only run into this if your cpan doesn't do recursive deps.
+       
+2002-02-20 20:45  jesse
+
+       * Makefile, lib/RT/Interface/Web.pm, webrt/Admin/Users/index.html,
+       webrt/Ticket/Update.html:
+
+       Bumped the version to 2.0.12-pre5.
+       
+       Web: Fixed a typo in user administration that prevented user listing
+       
+       Web: fixed recieve to receive in ticket/update
+       
+2002-02-19 03:23  jesse
+
+       * lib/RT/Scrips.pm:
+
+       Fixed a typo. (Added a missing ;)
+       
+2002-02-19 01:04  jesse
+
+       * Makefile:
+
+       Bumped version to 2.0.12pre4
+       
+2002-02-18 18:35  jesse
+
+       * lib/RT/Condition/Generic.pm:
+
+       RT-Ticket: 1194
+       
+       cleaned up a reference to "ApplicableTypes", a nonexistent parameter to Condition->new
+       
+2002-02-18 18:30  jesse
+
+       * webrt/Elements/Login:
+
+       RT-Ticket: 1226
+       RT-Status: resolved
+       
+       Added an explicit reset of the content-type to 'text/html' when displaying hte login page
+       
+2002-02-18 18:25  jesse
+
+       * webrt/Admin/Users/index.html:
+
+       rt-ticket: 1190
+       rt-status: resolved
+       
+       Modified administrative userlist to make it easier to click on users who have no Name attribute defined
+       
+2002-02-18 18:18  jesse
+
+       * webrt/: Elements/Header, SelfService/Elements/Header:
+
+       rt-ticket: 1176
+       rt-status: resolved
+       
+       Applied tom's patch which hides the preferences link if the user doesn't have the right to "modify self"
+       
+2002-02-18 18:14  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       RT-Ticket: 1165
+       RT-Status: resolved
+       
+       exporting the ParseAddressFromHeader subroutine so others can play with it
+       
+2002-02-18 18:06  jesse
+
+       * webrt/Ticket/Update.html:
+
+       RT-Ticket:1209
+       RT-Milestone: 2.0.x
+       RT-Status: resolved
+       
+       Removed  a bogus font tag from the Ticket update screen
+       
+2002-02-18 18:00  jesse
+
+       * lib/RT/Interface/Web.pm, webrt/Search/Listing.html:
+
+       RT-Ticket: 1243
+       RT-Status: resolved
+       RT-Milestone: 2.0.x
+       RT-Broken-In: 2.0.8
+       RT-Broken-In: 2.0.9
+       RT-Broken-In: 2.0.10
+       RT-Broken-In: 2.0.11
+       RT-Subsystem: HTML::Mason Frontend
+       RT-Severity: Normal
+       
+       Switched the web frontend to use an in-core scalar for uploaded attachment content,
+       rather than a tempfile which wasn't getting cleaned up properly
+       
+2002-02-18 16:53  jesse
+
+       * webrt/Search/Listing.html:
+
+       RT-Ticket: 1245
+       Rt-status: resolved
+       
+       Applied a patch to nuke duplicate restrictions in the webui.
+       
+2002-02-18 16:47  jesse
+
+       * webrt/Admin/Groups/Members.html:
+
+       RT-Ticket: 1421
+       RT-Status: resolved
+       
+       moved a label inside a loop to make the ui easier to understand
+       
+2002-02-18 16:36  jesse
+
+       * etc/config.pm:
+
+       Set Default for UseFriendlyToLines to 0 by default, to deal with users running
+       redhat who have trouble configuring RT.
+       
+2002-02-18 16:31  jesse
+
+       * Makefile, lib/RT/Scrip.pm, tools/insertdata:
+
+       Edited insertdata to insert scrips by default, so that users don't need
+       to go through the configuration task themselves.
+       
+       Change the Makefile's WEB_GROUP to www by default for redhat and OSX.
+       
+       Correced docs for lib/RT/Scrip new() method
+       
+2002-02-08 01:23  jesse
+
+       * webrt/SelfService/Display.html:
+
+       cleanup to "last trans" in SelfService
+       
+2002-02-08 00:53  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Generalized "Abort" function to allow other non-html error messages with
+       proper handlers
+       
+2002-02-08 00:49  jesse
+
+       * etc/: schema.Pg, schema.mysql:
+
+       removed duplicate indices
+       
+2002-02-07 16:41  jesse
+
+       * etc/config.pm:
+
+       Added some docs to the config file from Rich Lafferty
+       
+2002-02-04 12:37  jesse
+
+       * lib/RT/Template.pm:
+
+       Output template content to core rather than disk when parsing.
+       
+2002-01-28 01:01  jesse
+
+       * webrt/Ticket/Elements/ShowHistory:
+
+       closing lasttrans anchor
+       
+2002-01-28 00:59  jesse
+
+       * webrt/Ticket/Elements/ShowHistory:
+
+       Closing the #lasttrans anchor
+       
+2002-01-28 00:58  jesse
+
+       * lib/RT/Record.pm:
+
+       RT-Ticket: 1156
+       
+       Pulling forward the patch for 1156
+       
+2002-01-28 00:57  jesse
+
+       * lib/RT/Record.pm:
+
+       RT-Ticket: 1156
+       RT-Status: resolved
+       
+2002-01-28 00:47  jesse
+
+       * bin/mason_handler.fcgi:
+
+       Small fix to the fastcgi handler to make attachment display work better,
+       thanks to rich lafferty.
+       
+2002-01-28 00:47  jesse
+
+       * etc/config.pm:
+
+       Removed some extraneous slashes from the config file.
+       
+2002-01-28 00:46  jesse
+
+       * README:
+
+       Clarified some readme stuff
+       
+2002-01-28 00:44  jesse
+
+       * bin/rt-commit-handler:
+
+       Bringing forward a fix to the cvs commit handler to deal with branched
+       version #s.
+       
+2002-01-28 00:40  jesse
+
+       * Makefile:
+
+       Bumped the version to 2.1.1
+       
+2002-01-28 00:27  jesse
+
+       * etc/RT_Config.pm:
+
+       file RT_Config.pm was initially added on branch rt-2-1.
+       
+2002-01-28 00:27  jesse
+
+       * bin/enhanced-mailgate:
+
+       file enhanced-mailgate was initially added on branch rt-2-1.
+       
+2002-01-28 00:27  jesse
+
+       * bin/rt-commit-handler:
+
+       file rt-commit-handler was initially added on branch rt-2-1.
+       
+2002-01-28 00:27  jesse
+
+       * Makefile, bin/enhanced-mailgate, bin/mason_handler.fcgi, bin/rt,
+       bin/rt-commit-handler, bin/rt-mailgate, bin/rtadmin, bin/webmux.pl,
+       etc/RT_Config.pm, etc/acl.Oracle, etc/acl.Pg, etc/acl.mysql,
+       etc/config.pm, lib/RT.pm, lib/RT/Handle.pm, lib/RT/User.pm,
+       lib/RT/Watcher.pm, lib/RT/Interface/CLI.pm,
+       lib/RT/Interface/Email.pm, tools/cpan2rpm, tools/initdb,
+       tools/insertdata, tools/testdeps:
+
+       First RT 2.1 checkin. don't expect this to run.  (though it does here)
+       
+2002-01-25 17:37  jesse
+
+       * README:
+
+       Added a warning to the readme that 2.1 is scary and people shouldn't use it
+       
+2002-01-25 17:24  jesse
+
+       * Makefile:
+
+       Branching 2.1.0  and incrementing the makefile to 2.1.
+       
+       Welcome to the future.
+       
+2002-01-24 13:30  jesse
+
+       * lib/RT/Transaction.pm:
+
+       RT-Ticket: 1201
+       RT-Status: resolved
+       
+       Better transaction display for "text" and "message" parts.
+       
+2002-01-24 13:00  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       RT-Ticket: 1166
+       RT-Status: resolved
+       
+       Implemented better URL regexp matching.
+       
+2002-01-24 10:39  jesse
+
+       * bin/rt-mailgate:
+
+       Fixed a typo in a debug message in rt-mailgate
+       
+2002-01-24 10:34  jesse
+
+       * lib/RT/Template.pm:
+
+       Switched from bogus mime parsing to using MIME::Parser like we should have
+       from the get-go.
+       
+       Used perltidy to clean up template.pm before we started working on it.
+       
+2002-01-24 10:28  jesse
+
+       * lib/RT/Record.pm:
+
+       Added a check to LoadByCols which causes postgres mode to not try to lookup
+       lc(undef)
+       
+2002-01-24 10:23  jesse
+
+       * webrt/NoAuth/webrt.css:
+
+       Added a new style to the stylesheet to support some new reports
+       
+2002-01-24 10:21  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       mail gateway now unfolds long headers on parse.
+       
+2002-01-11 15:20  jesse
+
+       * Makefile, lib/RT/Transaction.pm:
+
+       RT-Ticket: 950
+       
+       Aoolyed the recommended patch to make blank bodies not get
+       mailed in lieu of the real message
+       
+2002-01-11 15:00  jesse
+
+       * Makefile, webrt/Ticket/ModifyAll.html:
+
+       Fixed a small bug that broke the "jumbo" page after 2.0.11
+       
+2002-01-11 01:17  jesse
+
+       * bin/rt-mailgate:
+
+       Added a new flag to rt-mailgate to enable setting the owner of new tickets based on +extension
+       
+2002-01-11 01:13  jesse
+
+       * webrt/Ticket/: Modify.html, ModifyAll.html:
+
+       Adding a couple ACL checks to better deal with moving tickets to queues the user can't see
+       
+2002-01-11 01:11  jesse
+
+       * webrt/Admin/Queues/index.html:
+
+       Fixed typo in ACL check that resulted in "Create Queue" being more restrictive than
+       it needed to be
+       
+2002-01-10 19:07  jesse
+
+       * webrt/Ticket/Update.html:
+
+       more tweaking
+       
+2002-01-10 19:05  jesse
+
+       * webrt/Ticket/Update.html:
+
+       Cleaned up the Ticekt Update ui some
+       
+2002-01-10 19:02  jesse
+
+       * lib/RT/: Attachment.pm, Action/Notify.pm, Condition/Generic.pm:
+
+       Cleanups to make Also-Cc and Also-Bcc go
+       
+2002-01-10 19:01  jesse
+
+       * lib/RT/Queue.pm:
+
+       Changed a test to work the way that Test::Inline does now
+       
+2002-01-10 18:34  jesse
+
+       * lib/RT/Attachment.pm:
+
+       Changes to attachment to provide the header frobbing necessary to send mail to ccs and bccs
+       
+2002-01-10 18:32  jesse
+
+       * Makefile, README, lib/RT/Ticket.pm, tools/insertdata,
+       webrt/Ticket/Update.html, lib/RT/Action/Notify.pm:
+
+       Work on "Explicit Cc" and "Explicit Bcc" for a client
+       
+2002-01-10 18:27  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       abstracting out a number to a named variable
+       
+2002-01-09 02:24  jesse
+
+       * webrt/Admin/: Groups/index.html, Keywords/index.html:
+
+       A couple tiny ui cleanups (removed some object ids from the UI where they were just being clutter
+       
+2002-01-02 21:58  jesse
+
+       * Makefile, README, webrt/Elements/Login:
+
+       Bumped some copyright notices to 2002. bumped version to 2.0.11
+       
+2001-12-26 14:59  jesse
+
+       * Makefile:
+
+       Bumping version to 2.0.11pre1
+       
+2001-12-26 14:51  jesse
+
+       * lib/RT/Tickets.pm:
+
+       Merges were being over-zealous in what they twiddled the effective id of.
+       
+       this meant that far too many tickets would show up in ticket listings.
+       
+2001-12-24 18:58  jesse
+
+       * Makefile:
+
+       Bumped the version to 2.0.10
+       
+2001-12-19 00:24  jesse
+
+       * Makefile:
+
+       Bumped version to 2.0.10pre4
+       
+       fix to makefile to genereate changelog for head
+       
+2001-12-18 03:58  jesse
+
+       * lib/RT/Tickets.pm:
+
+       The "null search" was finding all tickets in 2.0.10pre3.  Fixed in CVS
+       
+2001-12-18 03:53  jesse
+
+       * bin/rt:
+
+       fixed typos in bin/rt that stopped --limit-subject --limit-requestors and --limit-body from
+       working
+       
+2001-12-17 15:13  jesse
+
+       * Makefile:
+
+       Bumped the version to 2.0.10-test3
+       
+2001-12-17 14:58  jesse
+
+       * lib/RT/User.pm, tools/insertdata, webrt/Admin/Users/Modify.html:
+
+       RT-Ticket: 935
+       RT-Status: resolved
+       
+       cleaned up seph's patch. this enabled me to actually really properly support
+       users with no email address, which meant there were a couple other cleanups
+       to go through too.
+       
+2001-12-17 14:26  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Some small cleanups to the IsWatcher stuff.
+       
+       Added checks to make sure that watchers aren't duplicated to ticket.pm
+       
+2001-12-17 13:04  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       Fixed an unclosed anchor which caused IE to render ticket listings wrong.
+       
+2001-12-14 18:29  jesse
+
+       * lib/Makefile.PL, tools/testdeps:
+
+       Bumped DBIx::SearchBuilder dependency to 0.48
+       
+2001-12-14 18:28  jesse
+
+       * tools/testdeps, webrt/Elements/Login:
+
+       Removed code to special case for bugs in mason < 1.01.
+       Moved us up to a mason 1.02 dependency
+       
+2001-12-14 18:26  jesse
+
+       * bin/rt:
+
+       bin/rt: added support for --version, fixed --status = !closed, docced --merge-into
+       
+2001-12-14 18:25  jesse
+
+       * Makefile:
+
+       Some stylistic cleanups  to the makefile from blair.
+       
+2001-12-14 16:42  jesse
+
+       * bin/rtadmin:
+
+       rtadmin had some issues where it would assume a 'name' if called without --name for user group and queue editing.
+       
+2001-12-14 16:06  jesse
+
+       * lib/RT/Tickets.pm:
+
+       
+       Ticket listings will no longer show tickets which have been merged into others.
+       
+               -j
+       
+2001-12-14 15:27  jesse
+
+       * lib/RT/User.pm:
+
+       Prevent users from futzing with nobody or rt_System, unless you're setting an email address.
+       (Arguably, that's a bug too)
+       
+2001-12-14 14:03  jesse
+
+       * lib/RT/: ACE.pm, Group.pm, GroupMember.pm, Keyword.pm,
+       KeywordSelect.pm, Queue.pm, Scrip.pm, Template.pm, Ticket.pm:
+
+       Standardised on "Permission Denied" instead of having some "Permission denied". Thanks simon.
+       
+               -j
+       
+2001-12-14 13:46  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Reordered the order that Basics actions are committed, so that Queue changes
+       come after other changes, so that users don't move tickets out of a queue before they have a
+       chance to update them.
+       
+2001-12-13 02:18  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Addition to Ticket->Import, so you can set owner by name.
+       
+       Fix for a bug in Ticket->AddWatcher that would let privileged watchers without
+       email addresses add others as watchers.
+       
+2001-12-03 20:13  jesse
+
+       * Makefile, lib/RT/EasySearch.pm, lib/RT/Keyword.pm,
+       lib/RT/Record.pm, lib/RT/Tickets.pm, lib/RT/User.pm:
+
+       more work on making sure that only the things we want are case sensitive
+       (IE name, content email address should always be insensitive.  when loading a row by any field other than ID, that should be case-insensitive)
+       
+2001-12-03 19:17  jesse
+
+       * Makefile:
+
+       bumped the version  to 2.0.10-test1
+       
+2001-12-03 19:14  jesse
+
+       * lib/RT/EasySearch.pm:
+
+       We now default to case sensitive searches, rather than case-insensitive ones.
+       (This should speed up Pg a LOT. We'll be adding in case-insensitive searching
+       for the 13 attributes that matter:
+       
+       Watcher->Email
+       User->name
+       User->email
+       User->gecos
+       Ticket->Subject
+       Queue->name
+       KeywordSelect->name
+       Keyword->Name
+       ObjectKeyword->Name
+       Attachment->Subject
+       Attachment->Content
+       Attachment->Headers
+       
+2001-12-03 19:13  jesse
+
+       * webrt/: Admin/Queues/Modify.html, Admin/Queues/People.html,
+       Elements/GotoTicket:
+
+       Some small UI cleanups from Hakke
+       
+2001-11-29 03:50  jesse
+
+       * webrt/: Elements/SelectEqualityOperator, Elements/SelectOwner,
+       Search/PickRestriction:
+
+       SelectOwner now passes a ticket up the line.
+       
+       Priority can now have = and != searches
+       
+2001-11-29 03:49  jesse
+
+       * tools/insertdata:
+
+       Cleaned up a template to display Ticket subject, if no transaction subject is given.
+       
+2001-11-29 03:48  jesse
+
+       * webrt/Ticket/: Update.html, Elements/EditPeople:
+
+       Now pass in ticket Id, so that "owner" can be someone who only has rights to that tikcet.
+       
+2001-11-29 03:47  jesse
+
+       * lib/RT/Ticket.pm:
+
+       FinalPriority should never get set to null if a ticket doesn't have the attribute set on create
+       
+       Untake's arguments were debognifed
+       
+2001-11-29 03:45  jesse
+
+       * webrt/SelfService/Elements/Header:
+
+       "Logout" no longer shows up when using external auth with SelfService
+       
+2001-11-29 03:44  jesse
+
+       * bin/rt:
+
+       Fix for setting priority when creating tickets with the cli
+       
+2001-11-29 03:42  jesse
+
+       * bin/rt-mailgate:
+
+       Added support for --ticket-id-from-extension to rt-mailgate
+       
+2001-11-14 14:28  jesse
+
+       * Makefile:
+
+       Bumped the version to 2.0.9
+       
+2001-11-13 11:33  jesse
+
+       * Makefile, bin/initacls.Pg, bin/rt:
+
+       rt-ticket:1007
+       rt-status: resolved
+       
+       initacls.pg no longer has extranious spaces which break variable assignment for port and host.
+       
+       Bumped version to 2.0.9pre9
+       
+2001-11-12 13:19  jesse
+
+       * Makefile, bin/initacls.Pg:
+
+       Fix for #1007 (Typo in bin/initacls.Pg)
+       Bumped version to 2.0.9pre8
+       
+2001-11-09 18:24  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       URL higlighting should now work with mailto: urls
+       
+2001-11-09 18:15  jesse
+
+       * Makefile, webrt/Ticket/Elements/ShowTransaction:
+
+       Cleaned up the url highlighting code
+       
+       Bumped the version to 2.0.9pre7
+       
+2001-11-09 17:27  jesse
+
+       * Makefile:
+
+       Fixing the new taggy stuff
+       
+2001-11-09 17:23  jesse
+
+       * Makefile:
+
+       Fixing the new branchy stuff
+       
+2001-11-09 17:07  jesse
+
+       * Makefile, bin/rtadmin, etc/config.pm,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Added a bit of doc to etc/config.pm
+       
+       Modified ShowTransaction to more properly escape html
+       
+       Fixed some of the cli help for rtadmin
+       
+       Added support for branch specification to the makefile
+       
+2001-11-08 15:15  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       I can commit on the head. really.
+       
+2001-11-08 15:06  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Dealing with defined but blank Owner fields
+       
+2001-11-06 18:06  jesse
+
+       * tools/cpan2rpm, tools/initdb, tools/insertdata, tools/testdeps,
+       webrt/autohandler, webrt/index.html, webrt/Admin/index.html,
+       webrt/Admin/Elements/CreateQueueCalled,
+       webrt/Admin/Elements/CreateUserCalled,
+       webrt/Admin/Elements/EditUserComments,
+       webrt/Admin/Elements/GrantQueueRightsTo,
+       webrt/Admin/Elements/GroupTabs, webrt/Admin/Elements/Header,
+       webrt/Admin/Elements/ListGlobalKeywordSelects,
+       webrt/Admin/Elements/ListGlobalScrips,
+       webrt/Admin/Elements/ModifyKeyword,
+       webrt/Admin/Elements/ModifyKeywordSelect,
+       webrt/Admin/Elements/ModifyQueue,
+       webrt/Admin/Elements/ModifyTemplate,
+       webrt/Admin/Elements/ModifyUser,
+       webrt/Admin/Elements/QueueRightsForUser,
+       webrt/Admin/Elements/QueueTabs,
+       webrt/Admin/Elements/SelectKeywordSelect,
+       webrt/Admin/Elements/SelectModifyGroup,
+       webrt/Admin/Elements/SelectModifyKeyword,
+       webrt/Admin/Elements/SelectModifyKeywordSelect,
+       webrt/Admin/Elements/SelectModifyQueue,
+       webrt/Admin/Elements/SelectModifyUser,
+       webrt/Admin/Elements/SelectQueueRights,
+       webrt/Admin/Elements/SelectRights,
+       webrt/Admin/Elements/SelectScrip,
+       webrt/Admin/Elements/SelectScripAction,
+       webrt/Admin/Elements/SelectScripCondition,
+       webrt/Admin/Elements/SelectSingleOrMultiple,
+       webrt/Admin/Elements/SelectTemplate,
+       webrt/Admin/Elements/SelectUsers, webrt/Admin/Elements/SystemTabs,
+       webrt/Admin/Elements/Tabs, webrt/Admin/Elements/UserTabs,
+       webrt/Admin/Global/GroupRights.html,
+       webrt/Admin/Global/Keywords.html, webrt/Admin/Global/Scrips.html,
+       webrt/Admin/Global/Template.html,
+       webrt/Admin/Global/Templates.html,
+       webrt/Admin/Global/UserRights.html, webrt/Admin/Global/index.html,
+       webrt/Admin/Groups/Members.html, webrt/Admin/Groups/Modify.html,
+       webrt/Admin/Groups/Rights.html, webrt/Admin/Groups/index.html,
+       webrt/Admin/KeywordSelects/Modify.html,
+       webrt/Admin/KeywordSelects/index.html,
+       webrt/Admin/Keywords/Modify.html, webrt/Admin/Keywords/index.html,
+       webrt/Admin/Queues/Create.html,
+       webrt/Admin/Queues/GroupRights.html,
+       webrt/Admin/Queues/Keywords.html, webrt/Admin/Queues/Modify.html,
+       webrt/Admin/Queues/People.html, webrt/Admin/Queues/Scrips.html,
+       webrt/Admin/Queues/Template.html,
+       webrt/Admin/Queues/Templates.html,
+       webrt/Admin/Queues/UserRights.html, webrt/Admin/Queues/index.html,
+       webrt/Admin/Users/Modify.html, webrt/Admin/Users/Prefs.html,
+       webrt/Admin/Users/Rights.html, webrt/Admin/Users/index.html,
+       webrt/Elements/Checkbox, webrt/Elements/CreateTicket,
+       webrt/Elements/CustomHomepageHeader, webrt/Elements/Error,
+       webrt/Elements/Footer, webrt/Elements/GotoTicket,
+       webrt/Elements/Header, webrt/Elements/ListActions,
+       webrt/Elements/Login, webrt/Elements/MessageBox,
+       webrt/Elements/MyRequests, webrt/Elements/MyTickets,
+       webrt/Elements/Quicksearch, webrt/Elements/Refresh,
+       webrt/Elements/Section, webrt/Elements/SelectBoolean,
+       webrt/Elements/SelectDate, webrt/Elements/SelectDateRelation,
+       webrt/Elements/SelectDateType, webrt/Elements/SelectKeyword,
+       webrt/Elements/SelectKeywordOptions, webrt/Elements/SelectLinkType,
+       webrt/Elements/SelectMatch, webrt/Elements/SelectNewTicketQueue,
+       webrt/Elements/SelectOwner, webrt/Elements/SelectQueue,
+       webrt/Elements/SelectResultsPerPage,
+       webrt/Elements/SelectSortOrder, webrt/Elements/SelectStatus,
+       webrt/Elements/SelectTicketSortBy, webrt/Elements/SelectUsers,
+       webrt/Elements/SelectWatcherType, webrt/Elements/ShadedBox,
+       webrt/Elements/Submit, webrt/Elements/Tabs,
+       webrt/Elements/TitleBoxEnd, webrt/Elements/TitleBoxStart,
+       webrt/Elements/ViewUser, webrt/Elements/dayMenu,
+       webrt/Elements/monthMenu, webrt/Elements/yearMenu,
+       webrt/NoAuth/Logout.html, webrt/NoAuth/Reminder.html,
+       webrt/NoAuth/webrt.css, webrt/NoAuth/images/rt.jpg,
+       webrt/NoAuth/images/spacer.gif, webrt/Search/Bulk.html,
+       webrt/Search/Listing.html, webrt/Search/PickRestriction,
+       webrt/Search/RestrictSearch.html, webrt/Search/TicketCell,
+       webrt/SelfService/Closed.html, webrt/SelfService/Create.html,
+       webrt/SelfService/Display.html, webrt/SelfService/Error.html,
+       webrt/SelfService/Prefs.html, webrt/SelfService/Update.html,
+       webrt/SelfService/index.html,
+       webrt/SelfService/Attachment/dhandler,
+       webrt/SelfService/Elements/GotoTicket,
+       webrt/SelfService/Elements/Header,
+       webrt/SelfService/Elements/MyRequests,
+       webrt/SelfService/Elements/Tabs, webrt/Ticket/Create.html,
+       webrt/Ticket/Display.html, webrt/Ticket/History.html,
+       webrt/Ticket/Modify.html, webrt/Ticket/ModifyAll.html,
+       webrt/Ticket/ModifyDates.html, webrt/Ticket/ModifyLinks.html,
+       webrt/Ticket/ModifyPeople.html, webrt/Ticket/Update.html,
+       webrt/Ticket/Attachment/dhandler,
+       webrt/Ticket/Elements/AddWatchers,
+       webrt/Ticket/Elements/EditBasics, webrt/Ticket/Elements/EditDates,
+       webrt/Ticket/Elements/EditKeywordSelects,
+       webrt/Ticket/Elements/EditLinks, webrt/Ticket/Elements/EditPeople,
+       webrt/Ticket/Elements/EditWatchers,
+       webrt/Ticket/Elements/ShowBasics, webrt/Ticket/Elements/ShowDates,
+       webrt/Ticket/Elements/ShowDependencies,
+       webrt/Ticket/Elements/ShowHistory,
+       webrt/Ticket/Elements/ShowKeywordSelects,
+       webrt/Ticket/Elements/ShowLinks,
+       webrt/Ticket/Elements/ShowMemberOf,
+       webrt/Ticket/Elements/ShowMembers,
+       webrt/Ticket/Elements/ShowPeople,
+       webrt/Ticket/Elements/ShowReferences,
+       webrt/Ticket/Elements/ShowRequestor,
+       webrt/Ticket/Elements/ShowSummary,
+       webrt/Ticket/Elements/ShowTransaction, webrt/Ticket/Elements/Tabs,
+       webrt/Ticket/Elements/ToolBar, webrt/User/Prefs.html:
+
+       Merging rt-1-1 to the head.
+       
+       RT 1.0 now lives on the rt-1-0 branch.
+       
+2001-11-06 18:03  jesse
+
+       * Makefile, NEWS, README, TODO, bin/initacls.Oracle,
+       bin/initacls.Pg, bin/initacls.mysql, bin/mason_handler.fcgi,
+       bin/mason_handler.scgi, bin/rt, bin/rt-mailgate, bin/rtadmin,
+       bin/rtmux.pl, bin/testdeps.pl, bin/webmux.pl, docs/FAQ,
+       docs/FAQ.html, docs/README.docs, docs/Security,
+       docs/rt-templates.html, docs/rt_users_guide.html,
+       docs/design_docs/CARS, docs/design_docs/TransactionTypes.txt,
+       docs/design_docs/acls, docs/design_docs/basic-definitions.txt,
+       docs/design_docs/cli_spec, docs/design_docs/cvs_integration,
+       docs/design_docs/evil_plans, docs/design_docs/link-definitions.txt,
+       docs/design_docs/local_hacking,
+       docs/design_docs/subscription-definitions.txt,
+       docs/design_docs/users, etc/acl.Oracle, etc/acl.Pg, etc/acl.mysql,
+       etc/config.pm, etc/mysql.acl, etc/rt.spec, etc/schema,
+       etc/schema.Oracle, etc/schema.Pg, etc/schema.mysql, etc/schema.pm,
+       etc/suidrt.c, lib/MANIFEST, lib/MANIFEST.SKIP, lib/Makefile.PL,
+       lib/RT.pm, lib/test.pl, lib/RT/ACE.pm, lib/RT/ACL.pm,
+       lib/RT/Attachment.pm, lib/RT/Attachments.pm, lib/RT/CurrentUser.pm,
+       lib/RT/Date.pm, lib/RT/EasySearch.pm, lib/RT/Group.pm,
+       lib/RT/GroupMember.pm, lib/RT/GroupMembers.pm, lib/RT/Groups.pm,
+       lib/RT/Handle.pm, lib/RT/Keyword.pm, lib/RT/KeywordSelect.pm,
+       lib/RT/KeywordSelects.pm, lib/RT/Keywords.pm, lib/RT/Link.pm,
+       lib/RT/Links.pm, lib/RT/ObjectKeyword.pm, lib/RT/ObjectKeywords.pm,
+       lib/RT/Queue.pm, lib/RT/Queues.pm, lib/RT/Record.pm,
+       lib/RT/Scrip.pm, lib/RT/ScripAction.pm, lib/RT/ScripActions.pm,
+       lib/RT/ScripCondition.pm, lib/RT/ScripConditions.pm,
+       lib/RT/Scrips.pm, lib/RT/Template.pm, lib/RT/Templates.pm,
+       lib/RT/TestHarness.pm, lib/RT/Ticket.pm, lib/RT/Tickets.pm,
+       lib/RT/Transaction.pm, lib/RT/Transactions.pm, lib/RT/User.pm,
+       lib/RT/Users.pm, lib/RT/Watcher.pm, lib/RT/Watchers.pm,
+       lib/RT/Action/Autoreply.pm, lib/RT/Action/Generic.pm,
+       lib/RT/Action/Notify.pm, lib/RT/Action/NotifyAsComment.pm,
+       lib/RT/Action/OpenDependent.pm, lib/RT/Action/README.hackers,
+       lib/RT/Action/ResolveMembers.pm, lib/RT/Action/SendEmail.pm,
+       lib/RT/Action/SendPasswordEmail.pm,
+       lib/RT/Action/StallDependent.pm,
+       lib/RT/Condition/AnyTransaction.pm, lib/RT/Condition/Generic.pm,
+       lib/RT/Condition/NewDependency.pm,
+       lib/RT/Condition/StatusChange.pm, lib/RT/Interface/CLI.pm,
+       lib/RT/Interface/Email.pm, lib/RT/Interface/Web.pm:
+
+       Merging rt-1-1 to the head.
+       
+       RT 1.0 now lives on the rt-1-0 branch.
+       
+2001-11-06 17:57  jesse
+
+       * rt.spec, etc/rt.spec:
+
+       Cleaned up spec file goodness
+       
+2001-11-06 16:28  jesse
+
+       * Makefile:
+
+       Bumped version to 2.0.9pre5
+       
+2001-11-05 00:55  jesse
+
+       * rt.spec, lib/Makefile.PL, tools/testdeps:
+
+       Bumped the searchbuilder dependency to 0.47
+       
+2001-11-05 00:54  jesse
+
+       * webrt/Ticket/Update.html:
+
+       Named the ticket update form.
+       
+2001-11-05 00:52  jesse
+
+       * etc/config.pm, lib/RT/Handle.pm:
+
+       Added support for postgres' connections-over-ssl
+       
+2001-11-05 00:49  jesse
+
+       * webrt/Admin/Elements/SelectRights:
+
+       Updated ACL selecting UI to work properly with browsers that try to auto-select
+       a value in a SELECT
+       
+2001-11-02 01:39  jesse
+
+       * Makefile:
+
+       bumped the version to 2.0.9pre4
+       
+2001-11-02 01:31  jesse
+
+       * webrt/Ticket/: Display.html, ModifyAll.html:
+
+       Fix to not automatically record comments if nothing was typed.
+       
+2001-11-02 00:52  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Fix for duplicated requestors on merge. #791
+       
+2001-11-01 17:40  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       More tweaking transaction display
+       
+2001-11-01 17:36  jesse
+
+       * webrt/: Admin/Users/Modify.html, Ticket/Elements/ShowTransaction:
+
+       Fixed newlines between ticket body and headers.
+       
+       Fix for 934: creating users doesn't completely fail (new)
+       
+2001-11-01 17:24  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       Cleanups to ShowTransaction
+       
+2001-11-01 17:10  jesse
+
+       * lib/RT/: Keyword.pm, Interface/Web.pm:
+
+       Fix for loading queue (post 2.0.8) when called by name
+       Fix for _not_ trying to load keywords when called with no value.
+       
+2001-10-31 17:38  jesse
+
+       * Makefile:
+
+       Fixed the make dist
+       
+2001-10-31 02:56  jesse
+
+       * Makefile:
+
+       Bumped the version to 2.0.9pre3
+       
+2001-10-31 02:54  jesse
+
+       * lib/RT/Action/SendPasswordEmail.pm:
+
+       file SendPasswordEmail.pm was initially added on branch rt-1-1.
+       
+2001-10-31 02:54  jesse
+
+       * lib/RT/Action/SendPasswordEmail.pm:
+
+       Added an action to mail a password to the user.
+       
+2001-10-31 02:54  jesse
+
+       * bin/rt-mailgate:
+
+       added support to rt-mailgate for putting the queue name in a +extension
+       
+2001-10-31 02:52  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Tickets which are created in a "resolved" state will now have their "resolved"
+       date set right.
+       
+2001-10-31 02:51  jesse
+
+       * lib/RT/User.pm:
+
+       Started work on "Email password to user"
+       
+2001-10-31 02:48  jesse
+
+       * lib/RT/Watcher.pm:
+
+       Added a patch from Simon Cozens which makes sure that a requestor is always a user, rather than an email address.
+       
+2001-10-31 02:47  jesse
+
+       * lib/RT/Keyword.pm:
+
+       Added a helper Load function to Keyword to load by Id or Path
+       
+2001-10-31 02:42  jesse
+
+       * lib/RT/Template.pm:
+
+       
+       Template got new helper functions for loading queue and system templates.
+       
+2001-10-31 02:40  jesse
+
+       * bin/rt, bin/rtadmin, lib/RT/Interface/CLI.pm,
+       lib/RT/Interface/Email.pm:
+
+       Bringing forward a security fix from 2.0.8_01.  (nonusers could get superuser permissions from the CLI)
+       
+2001-10-31 02:37  jesse
+
+       * webrt/: Elements/MyRequests, Elements/MyTickets,
+       Ticket/Create.html:
+
+       Fixed a display bug in mytickets and myrequests which prevented clicking on
+       subjectless email
+       
+2001-10-31 02:04  jesse
+
+       * etc/: config.pm, schema.Pg, schema.mysql, schema.pm:
+
+       Lengthened queue name and email addresses in the default DB schema
+       
+2001-10-31 02:02  jesse
+
+       * webrt/Ticket/Update.html, lib/RT/Interface/Web.pm:
+
+       Now show the current ticket subject by default in the update subject box.
+       But don't include it in the transaction if it hasn't changed.
+       
+2001-10-31 01:09  jesse
+
+       * Makefile, bin/rt, bin/rt-mailgate, bin/rtadmin,
+       lib/RT/Interface/CLI.pm, lib/RT/Interface/Email.pm:
+
+       Security fixes per Jay at mojomole
+       
+2001-10-25 17:40  jesse
+
+       * webrt/Search/Listing.html:
+
+       Fixed a bug in new listing display. introduced after 2.0.8
+       
+2001-10-24 14:10  jesse
+
+       * Makefile, lib/RT/Ticket.pm:
+
+       Fixed the ticket status changes from open to open bug, thanks to raphael
+       at linkvest.
+       
+2001-10-23 17:34  jesse
+
+       * webrt/Ticket/Create.html:
+
+       Some cleanups to the Create form. No new functionality, just a little bit prettier
+       
+2001-10-23 17:34  jesse
+
+       * webrt/Elements/SelectTicketSortBy:
+
+       Elements/SelectTicketsSortBy now uses new list of sortable Tickets fields
+       in Tickets.pm
+       
+2001-10-23 17:32  jesse
+
+       * webrt/Search/Listing.html:
+
+       Column headings in searches are now clicky, where possible
+       
+2001-10-23 17:31  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       URLs in ticket history should now be clicky
+       
+2001-10-23 17:26  jesse
+
+       * webrt/Elements/CustomHomepageHeader:
+
+       file CustomHomepageHeader was initially added on branch rt-1-1.
+       
+2001-10-23 17:26  jesse
+
+       * webrt/: index.html, Elements/CustomHomepageHeader:
+
+       Added a hook for sites to put their own html in the top of the "Home" page
+       
+2001-10-23 17:24  jesse
+
+       * webrt/Elements/: SelectNewTicketQueue, SelectQueue:
+
+       Frefactored SelectQueue to elimintate dupicate code and enable a "too many queues" option
+       
+2001-10-23 17:22  jesse
+
+       * lib/: RT.pm, RT/Date.pm, RT/Keyword.pm, RT/KeywordSelect.pm,
+       RT/Keywords.pm, RT/Link.pm, RT/Links.pm, RT/ObjectKeywords.pm,
+       RT/Record.pm, RT/Scrip.pm, RT/ScripActions.pm,
+       RT/ScripConditions.pm, RT/Ticket.pm, RT/Action/SendEmail.pm,
+       RT/Condition/Generic.pm, RT/Interface/CLI.pm,
+       RT/Interface/Email.pm:
+
+       Simple fixes to POD from Feargal Reilly to fix complaints from pod2man
+       
+2001-10-23 17:08  jesse
+
+       * lib/RT/Tickets.pm:
+
+       Refactored Tickets.pm a bit to provide better access to fields
+       that tickets can be sorted on.
+       
+2001-10-23 17:06  jesse
+
+       * etc/: config.pm, schema.Pg, schema.mysql, schema.pm:
+
+       Added some new indices, based on recommendations from Nobel Tse at Outblaze
+       
+       Started to cleanup config.pm to not use deprecated methods when displaying
+       ticket columns.
+       
+2001-10-19 15:44  jesse
+
+       * Makefile, bin/initacls.Pg, bin/initacls.mysql, tools/initdb:
+
+       Reverting last patch. it lead to too much brokenness
+       
+2001-10-19 15:16  jesse
+
+       * Makefile, bin/initacls.Pg, bin/initacls.mysql, tools/initdb:
+
+       Work on the install procedure to automate it some more. (automatically supply
+       passwords defined in the makefile)
+       
+2001-10-19 00:49  jesse
+
+       * Makefile:
+
+       Bumped version to 2.0.9pre1
+       
+2001-10-19 00:46  jesse
+
+       * docs/design_docs/cvs_integration:
+
+       file cvs_integration was initially added on branch rt-1-1.
+       
+2001-10-19 00:46  jesse
+
+       * webrt/NoAuth/images/rt.jpg:
+
+       file rt.jpg was initially added on branch rt-1-1.
+       
+2001-10-19 00:46  jesse
+
+       * rt.spec, docs/design_docs/cvs_integration, etc/config.pm,
+       webrt/rt.jpg, webrt/Elements/Header, webrt/Elements/TitleBoxEnd,
+       webrt/Elements/TitleBoxStart, webrt/NoAuth/images/rt.jpg,
+       webrt/SelfService/Elements/Header:
+
+       Refactored images path to have a configurable URL, so it will work with fastcgi ;)
+       
+2001-10-19 00:37  jesse
+
+       * webrt/: Elements/Footer, autohandler:
+
+       Added support for timing of page display with &Debug=1
+       
+2001-10-19 00:29  jesse
+
+       * lib/RT/User.pm:
+
+       User.pm: a fix to allow you to create multiple users with no email address.
+       
+2001-10-19 00:25  jesse
+
+       * lib/RT/Tickets.pm:
+
+       Added sub Due to Tickets.pm. fixes #910
+       
+2001-10-19 00:17  jesse
+
+       * etc/rt.spec:
+
+       file rt.spec was initially added on branch rt-1-1.
+       
+2001-10-19 00:17  jesse
+
+       * Makefile, rt.spec, etc/rt.spec:
+
+       Some work on the rpm build infrastructure
+       
+2001-10-19 00:03  jesse
+
+       * tools/cpan2rpm:
+
+       cpan2rpm doesn't repeat builds of the same distribution, even if we're looking for differnet  modules
+       
+2001-10-18 23:30  jesse
+
+       * tools/cpan2rpm:
+
+       cpan2rpm has been cleaned up a whole lot. it should actually be somewhat smarter about not doing the same work twice
+       
+2001-10-18 22:42  jesse
+
+       * tools/cpan2rpm:
+
+       Added cpan2rpm to the tools directory, for autogenerating rpms of cpan modules
+       
+2001-10-18 22:42  jesse
+
+       * tools/cpan2rpm:
+
+       file cpan2rpm was initially added on branch rt-1-1.
+       
+2001-10-18 21:20  jesse
+
+       * Makefile, rt.spec:
+
+       Added support for autobuilding an rpm with "make rpm"
+       
+2001-10-18 02:15  jesse
+
+       * tools/insertdata:
+
+       Fixed templates to include the ticket subject if it was otherwise blank
+       
+2001-10-18 02:14  jesse
+
+       * lib/RT/Interface/Web.pm, webrt/Ticket/Display.html:
+
+       work on web-based ticket creation. fixed bugs setting due dates, etc
+       
+2001-10-18 02:13  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       fixed a user creation race condition in the mail gateway
+       
+2001-10-18 02:11  jesse
+
+       * webrt/Elements/SelectStatus:
+
+       use the new status abstraction in the web ui
+       
+2001-10-18 02:08  jesse
+
+       * tools/testdeps:
+
+       moved to a dbix::searchbuilder 0.46 dependency
+       
+2001-10-18 02:07  jesse
+
+       * lib/RT/Tickets.pm:
+
+       Movign away from a deprecated API
+       
+2001-10-18 02:07  jesse
+
+       * tools/initdb:
+
+       initdb won't whine if you don't set a host
+       
+2001-10-18 02:05  jesse
+
+       * docs/design_docs/evil_plans:
+
+       Added some docs about what 2.2 might hold
+       
+2001-10-18 02:00  jesse
+
+       * lib/RT/: Ticket.pm, Queue.pm:
+
+       Abstracted out status enumeration and validation
+       
+2001-10-18 01:57  jesse
+
+       * webrt/Ticket/Elements/EditLinks:
+
+       added WebPath to fix some links in showlinks
+       
+2001-10-17 16:34  jesse
+
+       * lib/Makefile.PL:
+
+       Bumped searchbuilder dependency to 0.46
+       
+2001-10-17 16:33  jesse
+
+       * webrt/Ticket/Elements/ShowHistory:
+
+       Added an optional bit of configuration to not show the "History" header
+       
+2001-10-06 03:01  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       Added a new option "Show commands" to show tranasaction
+       
+2001-10-06 03:00  jesse
+
+       * webrt/Admin/: Keywords/index.html, Queues/index.html:
+
+       Added support for reenabling deleted queues and keywords
+       
+2001-10-06 02:57  jesse
+
+       * lib/RT/Condition/Generic.pm:
+
+       Added test harness glue
+       
+2001-10-06 02:55  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Added code to detect bogus ticket update types
+       
+2001-10-06 02:54  jesse
+
+       * lib/RT/Interface/: CLI.pm, Email.pm:
+
+       Added support for unit testing
+       
+2001-10-06 02:51  jesse
+
+       * lib/RT/Handle.pm:
+
+       Added support for unit testing
+       
+       Added a test for Database Port definition to stop perl from whining
+       
+2001-10-06 02:49  jesse
+
+       * lib/RT/: ACE.pm, ACL.pm, Action/Generic.pm, Action/SendEmail.pm,
+       Attachment.pm, Attachments.pm:
+
+       Added support for unit testing
+       
+2001-10-06 02:46  jesse
+
+       * lib/RT/: Tickets.pm, Transaction.pm, Transactions.pm, User.pm,
+       Users.pm, Watcher.pm, Watchers.pm, Queues.pm, Record.pm, Scrip.pm,
+       ScripAction.pm, ScripActions.pm, ScripCondition.pm,
+       ScripConditions.pm, Scrips.pm, Template.pm, Templates.pm:
+
+       Added support for unit testing
+       
+2001-10-06 02:43  jesse
+
+       * lib/RT/: Keyword.pm, KeywordSelect.pm, KeywordSelects.pm,
+       Keywords.pm, Link.pm, Links.pm, ObjectKeyword.pm,
+       ObjectKeywords.pm, CurrentUser.pm, Date.pm, EasySearch.pm,
+       Group.pm, GroupMember.pm, GroupMembers.pm, Groups.pm:
+
+       Added support for unit testing
+       
+2001-10-06 02:38  jesse
+
+       * lib/RT.pm:
+
+       Added the beginnings of test support to RT.pm
+       
+2001-10-06 02:22  jesse
+
+       * lib/RT/TestHarness.pm:
+
+       file TestHarness.pm was initially added on branch rt-1-1.
+       
+2001-10-06 02:22  jesse
+
+       * lib/: Makefile.PL, RT/TestHarness.pm:
+
+       First bits of glue for new RT unit testing infrastructure
+       
+2001-10-06 02:19  jesse
+
+       * bin/rt:
+
+       bin/rt: Documentation cleanups. removing an unnecessary data tainting check
+       
+2001-10-06 02:18  jesse
+
+       * bin/initacls.Pg:
+
+       Fixes to initacls.Pg to allow installation with Unix Domain sockets
+       
+2001-10-04 02:01  jesse
+
+       * Makefile:
+
+       Bumped the version to 2.0.8
+       
+2001-10-01 02:44  jesse
+
+       * rt.spec:
+
+       Revved the version of DBIx::SearchBuilder needed in the .spec file
+       
+2001-10-01 02:42  jesse
+
+       * Makefile, lib/RT/Interface/Email.pm:
+
+       Fixed the call to MIME::Entity::Build in Email.pm
+       
+       Bumped the version to 2.0.8pre3
+       
+2001-09-30 23:03  jesse
+
+       * README, bin/rt, lib/RT/Tickets.pm, lib/RT/Interface/Email.pm,
+       webrt/Search/PickRestriction, webrt/Ticket/History.html,
+       webrt/Ticket/Elements/EditBasics,
+       webrt/Ticket/Elements/ShowHistory:
+
+       Code refactoring to allow searching for nonlocal links
+       Cleanup to the Loop prevention stuff (v. 2.0.8pre2)
+       Readme cleanup
+       CLI cleanups.  changed "return" to "exit"
+       
+2001-09-26 16:31  jesse
+
+       * tools/testdeps:
+
+       Bumped the searchbuilder dependency.  to 0.43
+       
+2001-09-26 16:28  jesse
+
+       * Makefile, bin/rt, lib/RT/Tickets.pm,
+       webrt/Admin/Queues/Scrips.html:
+
+       Added a fix for ItemsArrayRef to Tickets which causes the Next/Prev links in
+       the web ui to be smarter.
+       
+       Fixed a typo in SCrips.html
+       
+       Bumped the version to 2.0.8pre2
+       
+2001-09-24 20:10  jesse
+
+       * rt.spec:
+
+       file rt.spec was initially added on branch rt-1-1.
+       
+2001-09-24 20:10  jesse
+
+       * Makefile, rt.spec:
+
+       Added destdir support to allow building of RPMS.
+       added rt.spec for building of RPMS.
+       
+2001-09-21 16:51  jesse
+
+       * Makefile:
+
+       Bumped makefule version to 2.0.8pre1
+       
+2001-09-21 16:45  jesse
+
+       * bin/rt, lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       lib/RT/Interface/Web.pm, webrt/Ticket/Elements/ShowTransaction:
+
+       Fixed a typo in the bin/rt docs
+       Allowed "Force owner change" from bulk update screen.
+       
+2001-09-20 22:34  jesse
+
+       * bin/rt, lib/RT/Action/SendEmail.pm, lib/RT/Interface/Email.pm,
+       webrt/Search/Bulk.html, webrt/Ticket/Elements/ShowTransaction:
+
+       Fixes for:
+       
+       888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888862 Re: [rt-users] how does rt --limit-last-updated work?
+       
+       863 Ticket/Elements/ShowTransaction doesn't show headers sometimes
+       
+       Fix for a bug in the code that prevents RT from looping with itself.
+       
+       Start of work for "force change owner" in Bulk.html
+       
+2001-09-19 16:49  jesse
+
+       * bin/initacls.Pg, bin/rt, etc/config.pm, lib/RT/Queue.pm,
+       lib/RT/Ticket.pm, lib/RT/Tickets.pm, webrt/Elements/Header,
+       webrt/Search/Listing.html, webrt/Ticket/Create.html,
+       webrt/Ticket/Display.html, webrt/Ticket/ModifyAll.html,
+       webrt/Ticket/Update.html, webrt/Ticket/Elements/ShowLinks:
+
+       Fixed a bug that would cause installation on postgres to lose if $PORT wasn't
+       specified
+       
+       Changed " to ' in a few places in the config file, to make file and variable
+       names with embedded metacharacters not lose as badly
+       
+       Set some logical defaults for Queue->Create, so it's not as likely to fail
+       to create a queue if you leave out some fields
+       
+       Ticket->Create: now takes a Starts date.  Also, fixed some API docs.
+       
+       Tickets: Negative searching on fields like "subject" should now work
+       
+       WebUI:  Added a pragma NO-CACHE pseudo-header, to stop overzealous browsers from caching RT's pages
+       
+       WebUI: changed the "Bookmark this search" link to "Bookmarkable URL for this search"
+       
+       WebUI: ticket create now has a "more detail" section
+       
+       WebUI: ticket updates no longer have a default subject preset.
+       
+       WebUI: fixed a bug in ShowLinks that generated bogus urls within RT if RT wasn't at / on your server
+       
+2001-09-13 00:03  jesse
+
+       * Makefile:
+
+       Bumping the version to 2.0.7
+       
+2001-09-10 02:18  jesse
+
+       * Makefile:
+
+       Bumped the makefile version to 2.0.7pre1
+       
+2001-09-10 02:11  jesse
+
+       * bin/mason_handler.fcgi, lib/RT/User.pm:
+
+       Fixed #883, a permissions caching bug.
+       
+       Added the Text::Wrapper dependency to mason_handler.fcgi
+       
+2001-09-06 15:59  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Reverted reopen on correspondence to 1.0 behaviour
+       
+2001-09-06 15:41  jesse
+
+       * webrt/SelfService/Display.html:
+
+       make row coloring alternate in selfservice transaction history
+       
+2001-09-06 15:39  jesse
+
+       * webrt/SelfService/Display.html:
+
+       Fixed cell spacing in selfservice transaction display
+       
+2001-09-06 15:33  jesse
+
+       * bin/webmux.pl, webrt/SelfService/Display.html,
+       webrt/Ticket/Display.html, webrt/Ticket/Elements/ShowHistory,
+       webrt/Ticket/Elements/ShowKeywordSelects,
+       webrt/Ticket/Elements/ShowSummary,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Cleaned up the web ticket transaction display.
+       
+2001-09-06 15:32  jesse
+
+       * lib/RT/: ObjectKeyword.pm, Queue.pm:
+
+       Added some docs.
+       Started refactoring Ticket statuses.
+       
+2001-09-06 15:32  jesse
+
+       * lib/RT/Ticket.pm:
+
+       $Ticket->SetTold  now optionally takes a date to set it to.
+       
+2001-09-06 15:30  jesse
+
+       * bin/rt:
+
+       Fixing some docs in bin/rt
+       
+2001-09-05 11:00  jesse
+
+       * webrt/Elements/: Header, Refresh:
+
+       Fix for #823 from martin@schapendonk.org
+               Refresh now allows you to turn off refresh.
+       
+2001-09-01 19:01  jesse
+
+       * webrt/Elements/MessageBox:
+
+       Fix for [fsck.com #803] Jumbo recording comments accidentally
+       
+2001-09-01 18:23  jesse
+
+       * Makefile:
+
+       Bumped the version to 2.0.6
+       
+2001-08-28 15:49  jesse
+
+       * Makefile:
+
+       Bumped the version to pre7, fixed a typo in the makefile's install instructions.
+       
+2001-08-24 00:13  jesse
+
+       * lib/RT/Attachment.pm:
+
+       importing the Crit->crit typo fix from 2.0.5_01
+       
+2001-08-24 00:08  jesse
+
+       * Makefile, lib/RT/Interface/Email.pm:
+
+       Rolled in the 2.05 perl 5.005 compatibility fix.
+       bumped the version to 2.0.6pre6
+       
+2001-08-23 19:49  jesse
+
+       * bin/rt-mailgate, tools/initdb:
+
+       fixed initdb to not append a port= to the db connect string unless it needs one.
+       
+       added support for $SenderMustExistInExternalDatabase to mailgate.
+       
+2001-08-23 19:46  jesse
+
+       * lib/RT/Groups.pm:
+
+       Added a default alphabetical sort order to lib/RT/Groups
+       
+2001-08-23 19:45  jesse
+
+       * etc/: config.pm, schema.Pg, schema.mysql, schema.pm:
+
+       Added new indices to schema. Regenerated mysql and postgres schema.
+       
+       Cleaned up etc/config.pm's docs some more, based on comments from Christian Gimore
+       Added a default value for RT::OwnerEmail, which was documented by not defined
+       by default
+       
+2001-08-22 18:48  jesse
+
+       * bin/rt, lib/RT/Ticket.pm, webrt/Ticket/Update.html:
+
+       bin/rt --create now deals with watchers.
+       
+       fixed a typo in Ticket/Update.html introduced after 2.0.5_03
+       
+2001-08-22 01:37  jesse
+
+       * Makefile, lib/RT/Tickets.pm, webrt/Admin/Global/Keywords.html:
+
+       Bumped version to 2.0.6-pre5
+       
+       Fixed 'deep' merges.
+       
+       Fixed a typo in Global/Keywords
+       
+2001-08-22 00:41  jesse
+
+       * Makefile, lib/RT/Interface/Email.pm:
+
+       Fixed a couple of bugs in the new email db lookups
+       
+2001-08-21 23:57  jesse
+
+       * Makefile, bin/initacls.Oracle, bin/initacls.Pg,
+       bin/initacls.mysql, tools/initdb:
+
+       Added support for setting the database port for postgres and mysql
+       
+2001-08-21 23:48  jesse
+
+       * README:
+
+       Updated jesse's email address and shuffled things around a bit.
+       
+2001-08-21 23:45  jesse
+
+       * etc/config.pm:
+
+       Added more docs and updated the default config for the external db lookups
+       
+2001-08-21 23:35  jesse
+
+       * webrt/: index.html, NoAuth/webrt.css, Search/Listing.html,
+       Search/PickRestriction, Ticket/ModifyDates.html,
+       Ticket/Update.html, Ticket/Elements/ShowHistory,
+       Ticket/Elements/ShowTransaction:
+
+       Added support for autorefresh to / and /Search/Listing.html
+       
+       Tightened up Transaction display. Fixed 'full headers' display.
+       
+       Cleaned up the ui in ModifyDates a bit
+       
+       Cleaned up the ui in Ticket/Update.html
+       
+       fixed a few typos in the css file
+       
+2001-08-21 23:30  jesse
+
+       * lib/RT/Attachment.pm:
+
+       Added some documentation
+       
+2001-08-21 23:25  jesse
+
+       * bin/rt-mailgate:
+
+       Bringing forward a patch to the mailgate from 2.0.5_01
+       
+2001-08-21 23:19  jesse
+
+       * webrt/Elements/Refresh:
+
+       file Refresh was initially added on branch rt-1-1.
+       
+2001-08-21 23:19  jesse
+
+       * webrt/Elements/Refresh:
+
+       Commiting a Refresh control to cvs
+       
+2001-08-21 23:17  jesse
+
+       * bin/rtadmin:
+
+       Fixed cli group creation
+       
+2001-08-21 23:12  jesse
+
+       * webrt/Elements/MessageBox:
+
+       Fixed transaction quoting to use the new transaction content quoting method.
+       
+2001-08-21 23:07  jesse
+
+       * webrt/Elements/Login:
+
+       Display a version string in the login box.
+       
+2001-08-21 23:06  jesse
+
+       * webrt/Elements/Header:
+
+       Added support for refresh to webrt/Elements/Header
+       Don't display the logout link if using external auth.
+       
+2001-08-21 23:05  jesse
+
+       * webrt/Admin/Users/Modify.html:
+
+       Little bit of ui and code cleanup to the User editing page
+       
+2001-08-21 23:04  jesse
+
+       * lib/RT/Record.pm:
+
+       Fixed broken caching of the CreatorObj
+       
+2001-08-21 23:03  jesse
+
+       * lib/RT/Handle.pm:
+
+       Added support for Database port to Handle.pm
+       
+2001-08-21 22:50  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Added vivek khera's patch to clean up newline processing for incoming messages
+       
+       Removed debugging output from file attachment subroutine
+       
+       Added support for setting refresh interval to the search page
+       
+2001-08-21 22:46  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       Fixed the bug fixed in 2.0.5_?? which called the wrong function for finding
+       sender name
+       
+       Altered the mail interface's external user lookup function to be returned
+        a hash of parameters
+       
+2001-08-21 22:43  jesse
+
+       * lib/RT/Transaction.pm:
+
+       Added support for Quoting a transaction to the Content sub.
+       
+2001-08-16 00:23  jesse
+
+       * Makefile, tools/testdeps:
+
+       dependency on CGI::Cookie 1.20 had reverted.
+       Bumped version to 2.0.5_03
+       
+2001-08-15 16:44  jesse
+
+       * Makefile, bin/rt-mailgate, lib/RT/Interface/Email.pm:
+
+       A couple of fixes to the mail gateway to deal with proper processing
+       of sender email addresses
+       
+2001-08-15 16:12  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       Trying for 2.0.5-01 again
+       Fixed a perl 5.6ism in Interface/Email
+       Fixed a bug in Attachment.pm that would cause a DIE on a fatal error
+       
+2001-08-15 16:12  jesse
+
+       * lib/RT/Attachment.pm:
+
+       Trying for 2.0.5-01 again
+       Fixed a bug in Attachment.pm that would cause a DIE on a fatal error
+       
+2001-08-15 16:12  jesse
+
+       * Makefile:
+
+       Trying for 2.0.5-01 again
+       
+2001-08-15 15:44  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       Fixed an error message in Attachment that should never be reached
+       Bumped the Makefile version to 2.0.5_01
+       Fixed an inadvertent perl 5.6ism in the mail gateway.
+       
+2001-08-15 15:42  jesse
+
+       * lib/RT/Attachment.pm:
+
+       Fixed an error message in Attachment that should never be reached
+       
+       Bumped the Makefile version to 2.0.5_01
+       
+2001-08-15 15:42  jesse
+
+       * Makefile:
+
+       Bumped the Makefile version to 2.0.5_01
+       
+2001-08-15 01:01  jesse
+
+       * Makefile:
+
+       Bumped version to 2.0.6-pre1
+       
+2001-08-15 01:00  jesse
+
+       * webrt/: autohandler, Admin/Users/Modify.html, Elements/Login,
+       SelfService/Prefs.html, User/Prefs.html:
+
+       Added support for external authentication to the web ui
+       
+2001-08-15 01:00  jesse
+
+       * bin/rt-mailgate, lib/RT/Interface/Email.pm:
+
+       Added support for looking up new users in an external datasource
+       
+2001-08-15 00:58  jesse
+
+       * etc/config.pm:
+
+       Added new configuration variables for external authentication for the web ui and
+       to support looking up new users in the mail gateway from a site-defined external datasource.
+       
+2001-08-15 00:17  jesse
+
+       * tools/testdeps:
+
+       Fixed testdeps to depend on the right version of CGI.pm
+       
+2001-08-14 23:55  jesse
+
+       * Makefile:
+
+       Released RT 2.0.5  No changes since 2.0.5-test3.
+       
+2001-08-12 21:13  jesse
+
+       * Makefile, bin/webmux.pl:
+
+       Fixed a typo in webmux.pl created after 2.0.4
+       Bumped the version to 2.0.5-test3
+       
+2001-08-12 20:00  jesse
+
+       * docs/manual.pod, lib/RT/Transaction.pm:
+
+       Core:  Updated Transaction->Content to be smarter about showing the first
+       message in the transaction.
+       
+       Removed the old manual skeleton in docs in favor of the web based docs.
+       
+2001-08-12 19:58  jesse
+
+       * lib/RT/Attachment.pm:
+
+       Core: Added a convenience function to RT::Attachemnt to find Children of this attachment.
+       Core: Added code to RT::Attachemnt to allow access to the 'Parent' attribute
+       
+2001-08-12 19:55  jesse
+
+       * lib/RT/Attachments.pm:
+
+       Core: A convenience function to search for Attachments by ContentType.
+       Core: Added some docs to RT::Attachments
+       
+2001-08-12 19:53  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       WebUI: Show 'comment' and 'correspond' for multipart messages in the per-transaction listing.
+       
+2001-08-10 23:59  jesse
+
+       * README:
+
+       README: added more pointers to the 'complete' docs.
+       README: Removed the little bit of configuration info in the readme in favor of the 'complete' docs on the web
+       
+2001-08-10 23:58  jesse
+
+       * bin/rt-mailgate:
+
+       mailgate: RT is now less likely to try to send mail to senders which will cause loops
+       
+2001-08-10 23:57  jesse
+
+       * webrt/index.html:
+
+       Webui: [home] now gets greyed out when you're there
+       
+2001-08-10 15:32  jesse
+
+       * Makefile, webrt/Elements/MessageBox:
+
+       Added a pair of () in webrt/Elements/MessageBox to correct a typo in an if
+       Bumped version to 2.0.5-test2
+       
+2001-08-10 00:24  jesse
+
+       * Makefile:
+
+       Bumped version to 2.0.5test1
+       
+2001-08-10 00:24  jesse
+
+       * lib/RT/Users.pm, webrt/Elements/MessageBox,
+       webrt/Ticket/Elements/AddWatchers, webrt/Ticket/Elements/EditDates:
+
+       Cleanup to date editing ui
+       
+       Code cleanups to Elements/MessageBox for more readable and maintainable code
+       Code cleanups to TicketElements/AddWatchers for more readable and maintainable code
+       
+2001-08-10 00:23  jesse
+
+       * webrt/Ticket/Elements/: EditLinks, ShowLinks:
+
+       Cleanup to Relationship editing and display ui
+       
+2001-08-10 00:21  jesse
+
+       * webrt/Ticket/ModifyDates.html:
+
+       UI cleanups in Ticket/ModifyDates.html
+       
+2001-08-10 00:20  jesse
+
+       * webrt/Ticket/ModifyAll.html:
+
+       Jumbo no longer loses ticket update contents.
+       
+2001-08-09 20:07  jesse
+
+       * webrt/Admin/Users/index.html:
+
+       Cleaned up Admin/Users search functionality and made it look more like
+       other user searching screens and enables searching for disabled users.
+       
+2001-08-09 20:02  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Properly strip trailing spaces from things we're trying to link to.
+       
+2001-08-09 20:00  jesse
+
+       * lib/RT/Links.pm:
+
+       By default, try to sort Links by something intelligent when displaying them
+       
+2001-08-03 01:52  jesse
+
+       * lib/RT/Action/Autoreply.pm:
+
+       Autoreplies are now from "Queuename" rather than from "RT"
+       
+2001-08-02 04:57  jesse
+
+       * webrt/: Admin/Queues/People.html, Elements/Quicksearch,
+       Elements/SelectResultsPerPage, Elements/SelectStatus,
+       Search/PickRestriction:
+
+       WebUI: Fixed a typo in hte 'bookmark this search' link display.
+       WebUI: By default, limit displayed results to 50 per page.
+       
+2001-08-02 04:57  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Documented a function in the web ui library
+       
+2001-08-02 04:56  jesse
+
+       * etc/config.pm:
+
+       Fixed a typo in the docs in the config file, thanks to sheeri.
+       
+2001-08-02 04:55  jesse
+
+       * bin/rt-mailgate:
+
+       rt-mailgate: bug-fix to properly grab 'from' addresses for purposes of determinigng whether something's a mailer-daemon
+       
+2001-08-02 04:54  jesse
+
+       * bin/rt:
+
+       Added support for merging to the CLI
+       Cli now works as described when linking tickets. Formerly, you couldn't admit the "+" before an added link
+       
+2001-08-02 04:53  jesse
+
+       * webrt/User/Prefs.html:
+
+       Bugfix to allow users to fully delete their rt signatures
+       
+2001-07-30 03:54  jesse
+
+       * docs/design_docs/evil_plans:
+
+       file evil_plans was initially added on branch rt-1-1.
+       
+2001-07-30 03:54  jesse
+
+       * Makefile, docs/design_docs/evil_plans:
+
+       bumped the version, so as not to get confused users complaining to the lists
+       
+       added some notes about what may happen for 2.2
+       
+2001-07-30 03:52  jesse
+
+       * webrt/SelfService/Display.html:
+
+       file Display.html was initially added on branch rt-1-1.
+       
+2001-07-30 03:52  jesse
+
+       * bin/mason_handler.fcgi, bin/webmux.pl, lib/RT/Interface/Web.pm,
+       webrt/SelfService/Create.html, webrt/SelfService/Details.html,
+       webrt/SelfService/Display.html, webrt/SelfService/Update.html,
+       webrt/SelfService/Elements/GotoTicket,
+       webrt/SelfService/Elements/MyRequests, webrt/Ticket/Create.html,
+       webrt/Ticket/Display.html, webrt/Ticket/Modify.html,
+       webrt/Ticket/ModifyAll.html, webrt/Ticket/ModifyDates.html,
+       webrt/Ticket/ModifyLinks.html, webrt/Ticket/ModifyPeople.html,
+       webrt/Ticket/Update.html, webrt/Ticket/Elements/EditBasics,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Switched SelfService/Details to SelfService/Display  to better jibe
+       with the regular UI.
+       
+       Added better current-page navigation hints to most per-ticket pages.
+       
+       Added support for uploading attachments on ticket create or update.
+       
+2001-07-30 03:51  jesse
+
+       * webrt/Admin/Elements/ListGlobalScrips:
+
+       file ListGlobalScrips was initially added on branch rt-1-1.
+       
+2001-07-30 03:51  jesse
+
+       * webrt/Admin/: Elements/ListGlobalKeywordSelects,
+       Elements/ListGlobalScrips, Global/Scrips.html,
+       Queues/Keywords.html, Queues/Scrips.html:
+
+       Added some ui to show global scrips and keywordselects in the queue
+       uis for adminning the same.
+       
+2001-07-30 03:51  jesse
+
+       * webrt/Admin/Elements/ListGlobalKeywordSelects:
+
+       file ListGlobalKeywordSelects was initially added on branch rt-1-1.
+       
+2001-07-30 03:49  jesse
+
+       * TODO:
+
+       Clarified the errata urls
+       
+2001-07-30 03:48  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Cleaned up the return values from Ticket->CreateLink. (always return $status, $msg);
+       
+2001-07-30 03:48  jesse
+
+       * lib/RT/Action/SendEmail.pm:
+
+       changed the case of the Managed-by header to be less grating
+       
+2001-07-30 03:45  jesse
+
+       * bin/rt-mailgate, lib/RT/Interface/Email.pm:
+
+       Moving a bunch of mail gateway library routines to the library where they
+       belong
+       
+2001-07-27 01:06  jesse
+
+       * README:
+
+       fixed a typo in the cronjob
+       
+2001-07-25 01:05  jesse
+
+       * bin/rt-mailgate, lib/RT/Tickets.pm:
+
+       Fix for mailgateway being unable to send error mail with 'sendmailpipe' mailing
+       Fix for 'blank' searches when using search bookmarking.
+       
+2001-07-25 01:00  jesse
+
+       * Makefile:
+
+       Bumped version to 2.0.4
+       Fixed bookmarkable searches
+       Fix for sendmailpipe errors in mailgateway
+       
+2001-07-24 12:10  jesse
+
+       * Makefile, webrt/Search/PickRestriction:
+
+       Fixed a bug in Search/PickRestriction, thanks to Vivek Khera
+       Bumped version to 2.0.3
+       
+2001-07-24 00:28  jesse
+
+       * Makefile:
+
+       Bumped version to 2.0.2
+       
+2001-07-24 00:21  jesse
+
+       * lib/RT/Tickets.pm, webrt/Elements/Submit,
+       webrt/Search/Listing.html, webrt/Search/PickRestriction:
+
+       Bookmarked searches now keep track of sorting and record count limits
+       New "Refine" button to allow faster updates of search criteria.
+       
+2001-07-24 00:01  jesse
+
+       * bin/rt:
+
+       Added support for --limit-queue to  RT cli tool
+       
+2001-07-23 10:43  jesse
+
+       * Makefile, tools/testdeps:
+
+       Testdeps was accidentally looking for Freeze::Thaw, rather than FreezeThaw
+       
+2001-07-23 00:31  jesse
+
+       * Makefile:
+
+       bumped version to 2.0.2test2
+       
+2001-07-23 00:30  jesse
+
+       * lib/RT/Tickets.pm, lib/RT/Interface/Web.pm, tools/testdeps,
+       webrt/Search/Listing.html:
+
+       Added support for bookmarkable searches.
+       
+2001-07-22 22:12  jesse
+
+       * lib/RT/: KeywordSelect.pm, User.pm:
+
+       Better error checking in User ACL checking
+       
+       Fixed an ACL check bug in KeywordSelect.pm, thanks to Matthew Stock.
+       
+2001-07-22 22:09  jesse
+
+       * webrt/Admin/Users/index.html:
+
+       Fix for a users search bug and a clarification of another search option.
+       
+2001-07-22 22:06  jesse
+
+       * lib/RT/: Keywords.pm, Queues.pm, Users.pm:
+
+       By default, order Queues, Users and Keywords alphabetically
+       
+2001-07-22 22:05  jesse
+
+       * Makefile:
+
+       Permissions fix for the data dirs for the fastcgi handler
+       
+       rt/etc is now mode 755, rather than 555.
+       
+2001-07-22 22:03  jesse
+
+       * bin/mason_handler.fcgi:
+
+       fastcgi handler no longer drops setgidness, so it can deal with session files.
+       
+2001-07-22 21:55  jesse
+
+       * etc/config.pm:
+
+       Cleaned up the config file a bit to make installation easier.
+       
+2001-07-22 21:52  jesse
+
+       * webrt/Elements/MyRequests:
+
+       Clean up phrasing of the "Tickets I own dialogbox"
+       
+2001-07-19 00:57  jesse
+
+       * Makefile:
+
+       Changed Makefile to not use MAN[13]INSTALLSITEPATH per Robert Shaw's changes
+       
+2001-07-18 16:20  jesse
+
+       * Makefile, lib/RT/Queue.pm:
+
+       Added a newline to Queue.pm to fix a pod bug
+       
+       Bumped version to 2.0.2-test1  to get some feedback
+       
+2001-07-18 16:16  jesse
+
+       * lib/RT/User.pm:
+
+       RT should now honor queue-level pseudogroup memebership for ACL checks.
+       Also, simplified some of the queue related ACL checking code.
+       
+       Reduced the ACL permissions cache from 30sec to 10sec.
+       
+2001-07-18 16:13  jesse
+
+       * lib/RT/Transaction.pm:
+
+       Added BriefDescription, which is just like Description without the "By actor"
+       at the end
+       
+       Descriptions Keyword operations should include _which_ keyword select from
+       here on in (also touched the last update to Ticket.pm)
+       
+2001-07-18 16:06  jesse
+
+       * lib/RT/Ticket.pm:
+
+       TTicket->Create no longer just adds admin ccs..unless the person doing the add
+       has the right to do so.  otherwise, there was an ACL related attack that
+       could happen. luckily none of the UI exposedd this hole yet.
+       
+       Ticket->Create should now be able to set keywords on create
+       
+       Ticket->Create now reports back non-fatal errors and how they were dealt with
+       
+       Ticket->AddKeyword got split into AddKeyword and _AddKeyword. The former
+       has the acl check and calls the latter. the latter just does the adding.
+       
+       Ticket->AddKeyword got a Silent flag, which causes a transaction not
+       to be written. useful on ticket create.
+       
+2001-07-18 15:59  jesse
+
+       * tools/testdeps:
+
+       Added tests for Apache::Cookie
+       and DBI 1.18 to testdeps
+       
+2001-07-18 15:53  jesse
+
+       * webrt/SelfService/: Details.html, Update.html:
+
+       Added the ability for requestors to use the self-service ui to comment on / reply to tickets.
+       
+2001-07-18 15:53  jesse
+
+       * webrt/SelfService/Update.html:
+
+       file Update.html was initially added on branch rt-1-1.
+       
+2001-07-17 22:12  jesse
+
+       * webrt/Admin/Queues/People.html:
+
+       Fixed a link to Users/Modify.html in Queue/People.html
+       
+2001-07-17 22:01  jesse
+
+       * bin/webmux.pl:
+
+       Fix to the solaris chown problem
+       
+2001-07-17 18:37  jesse
+
+       * README:
+
+       Fixed typo in the cron job description
+       
+2001-07-13 02:17  jesse
+
+       * lib/RT/Tickets.pm:
+
+       Work on tickets.pm to support some new searching options
+       
+2001-07-12 19:43  jesse
+
+       * Makefile:
+
+       Bumped version to 2.0.1
+       
+2001-07-11 23:17  jesse
+
+       * etc/config.pm:
+
+       No longer log things of less than 'error' to the logfile
+       
+2001-07-11 23:16  jesse
+
+       * Makefile, bin/mason_handler.fcgi, bin/rt, bin/rt-mailgate,
+       bin/rtadmin, lib/Makefile.PL, lib/RT.pm, lib/RT/Interface/CLI.pm,
+       lib/RT/Interface/Email.pm:
+
+       Make sure that the setgid programs don't drop setgidness before trying to open logfiles
+       
+2001-07-10 20:15  jesse
+
+       * Makefile, lib/Makefile.PL, lib/RT/Tickets.pm,
+       lib/RT/Interface/Web.pm, tools/testdeps:
+
+       Fixes to allow search-by-requestor to search for requestors who don't have RT accounts
+       
+2001-07-10 13:53  jesse
+
+       * bin/webmux.pl:
+
+       Fix for the weird sessiondata permissions bug folks have seen on solaris
+       
+2001-07-10 11:47  jesse
+
+       * lib/RT/Tickets.pm:
+
+       Change to Tickets.pm to fix description of watcher limits
+       
+2001-07-10 11:36  jesse
+
+       * bin/webmux.pl, webrt/Elements/SelectMatch:
+
+       Fixed a bug in webmux.pl that prevented Apache::Cookie support from working
+       
+       Fixed a bug in the enw SelectMatch code
+       
+2001-07-10 11:20  jesse
+
+       * lib/RT/Ticket.pm, webrt/Elements/SelectMatch,
+       webrt/Search/PickRestriction:
+
+       Cleanup to SelectMatch and PickRestriction to clear up the UI issues
+       with searching for Tickets from a give nrequestor
+       
+2001-07-09 10:36  jesse
+
+       * lib/RT/Template.pm:
+
+       Fixed a typo in Template.pm that squashed templates on outgoing mail. (Bug in my untainting fix)
+       
+       Bumped version to 2.0.1-test2
+       
+2001-07-09 10:36  jesse
+
+       * Makefile:
+
+       Fixed a typo in Template.pm that squashed templates on outgoing mail. (Bug in my untainting fix)
+       
+2001-07-08 22:31  jesse
+
+       * README:
+
+       Added some polite comments about supporting the development of RT
+       with contracts, cash or gifts
+       
+2001-07-08 20:14  jesse
+
+       * Makefile, bin/mason_handler.fcgi,
+       webrt/SelfService/Elements/ShowTransaction:
+
+       Bumped version to 2.01pre1
+       
+       Fixed mason_handler header newlines
+       
+2001-07-08 20:12  jesse
+
+       * lib/RT/: ScripAction.pm, ScripCondition.pm, Template.pm,
+       Ticket.pm:
+
+       Taint fixes for ScripAction, ScripCondtion,Template.
+       Header wrapping fix in Template.pm.
+       Fix in Ticket.pm to make EffectiveId a public field.
+       
+2001-07-06 18:32  jesse
+
+       * lib/RT/Attachment.pm:
+
+       Applied a patch from ivan to deal with poorly formed mime messages
+       and clean up the attachment proccessing code.
+       
+2001-07-06 18:26  jesse
+
+       * webrt/SelfService/Attachment/dhandler:
+
+       file dhandler was initially added on branch rt-1-1.
+       
+2001-07-06 18:26  jesse
+
+       * webrt/: SelfService/Details.html,
+       Ticket/Elements/ShowTransaction, SelfService/Attachment/dhandler:
+
+       ShowTransaction now no longer displays 'Comment' and 'Reply' if the user
+       doesn't have the right rights.
+       
+       SelfService uses the 'standard' ShowTransaction.
+       
+       SelfService now displays Attachments
+       
+2001-07-06 18:21  jesse
+
+       * etc/schema.mysql:
+
+       Switched mysql to use LONGTEXT rather than LONGBLOB to get
+       case insensitive searching.
+       
+2001-07-06 18:19  jesse
+
+       * bin/mason_handler.fcgi, lib/RT/Interface/Web.pm,
+       webrt/NoAuth/Logout.html:
+
+       Cleanups to deal better with the fastcgi handler.
+       
+       Switched to CGI::Fast from FCGI
+       
+       ContentType fixes.
+       
+       Logging out now clears the session hash, rather than detaching a perfectly good session. (This gets around an obnoxious tainting issue too)
+       
+2001-07-06 16:14  jesse
+
+       * webrt/Ticket/Attachment/dhandler:
+
+       Changed default attachment type to text/plain from text/html
+       
+2001-07-05 16:48  jesse
+
+       * bin/rt-mailgate, lib/RT/Ticket.pm:
+
+       Updates to make Correspondence on a closed or stalled ticket reopen the ticket
+       
+2001-07-04 00:51  jesse
+
+       * bin/rt:
+
+       bin/rt will now show transactions of type 'text'
+       
+2001-07-04 00:36  jesse
+
+       * bin/webmux.pl:
+
+       Minor fixes to webmux.pl for content type stuff.
+       
+2001-07-03 23:15  jesse
+
+       * lib/RT/Interface/Web.pm, webrt/Admin/Queues/index.html,
+       webrt/Search/PickRestriction:
+
+       Web ui now supports 'SearchByPriority'
+       
+2001-07-03 23:14  jesse
+
+       * etc/config.pm:
+
+       Added a few comments about configuration variables
+       
+2001-07-03 23:12  jesse
+
+       * webrt/SelfService/Elements/ShowTransaction:
+
+       Now SelfService displays attachments of type 'text'
+       
+2001-07-03 23:08  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       With the web ticket display, now display attachments of type 'text' properly.
+       Also, always have a download link.
+       
+2001-07-03 23:00  jesse
+
+       * webrt/Ticket/Elements/ShowLinks:
+
+       Changed SubTickets/SuperTickets to Parents/Children
+       
+2001-07-03 22:57  jesse
+
+       * Makefile:
+
+       Added notes to the effect that the speedycgi and fastcgi handlers aren't
+       supported.
+       
+2001-07-03 22:52  jesse
+
+       * bin/webmux.pl, bin/mason_handler.fcgi,
+       webrt/Ticket/Attachment/dhandler:
+
+       Switched mod_perl hander to use Apache::Cookie
+       
+       Refactored mime type handling to properly set the mime type with the modperl
+       handler and the fastcgi handler
+       
+       Added untainting for the requirred parts of the fastcgi wrapper.
+       I'm not yet confident that it works properly, but it does the right
+       thing in the trivial case.
+       
+2001-06-27 00:11  jesse
+
+       * Makefile:
+
+       Bumped version to 2.0 for release to the unsuspecting masses
+       
+2001-06-25 16:28  jesse
+
+       * Makefile, lib/RT/Ticket.pm:
+
+       Fix for a bug introduced by RC2  (while fixing another bug with _Links)
+        which caused Links listings to recurse rather infinitely.
+       
+       Bumped RC2 to RC3
+       
+2001-06-25 16:25  jesse
+
+       * webrt/Admin/Elements/SelectRights:
+
+       Fix for a crashing bug when editing ACLs without permission
+       
+2001-06-25 12:19  jesse
+
+       * Makefile, lib/RT/Ticket.pm:
+
+       Fixed bug in Ticket.pm -> _Links that caused it to return an error
+       rather than an empty links object if permission denied.
+       
+       Bumped version to RC2
+       
+2001-06-23 02:48  jesse
+
+       * Makefile, README, tools/import-1.0-to-2.0:
+
+       removed 1.0-2.0 importer. (it's now in the contrib archive)
+       updated readme and makefile to know about the change.
+       
+       Bumped the version to 2.0.0-RC1. Yeah. That's right. the major version
+       number just got incremented.
+       
+2001-06-22 14:50  jesse
+
+       * Makefile, lib/RT/Ticket.pm:
+
+       hopefully a fix for everyone's favorite import bug.
+       
+2001-06-20 23:56  jesse
+
+       * webrt/Search/Bulk.html:
+
+       Display cleanups to Bulk.html.
+       Removed bogus nav items
+       
+2001-06-20 17:33  jesse
+
+       * NEWS, lib/Makefile.PL:
+
+       Removed the out of date NEWS file. It's been superceeded by the changelog
+       
+2001-06-20 17:27  jesse
+
+       * Makefile, README, lib/RT/Ticket.pm, lib/RT/User.pm,
+       lib/RT/Interface/Web.pm, tools/import-1.0-to-2.0, tools/testdeps,
+       webrt/Ticket/Display.html, webrt/Ticket/History.html:
+
+       Fixes for the import problems some folks have been seeing
+       Fixes for the Can't click on URL issues reported by mixo and Carl Potter.
+       Turned off some overly verbose debugging.
+       
+       Bumped the version to 1.3.105
+       
+2001-06-19 00:34  jesse
+
+       * Makefile:
+
+       Bumped version to 1.3.104
+       
+2001-06-19 00:25  jesse
+
+       * lib/RT/Tickets.pm:
+
+       A fix for search based on keywords that don't exist. (Some rows would be lost, due to poorly constructed SELECT statements.  This change requires DBIx::SearchBuilder 0.39
+       
+2001-06-18 15:39  jesse
+
+       * Makefile, README, webrt/Ticket/Elements/ShowTransaction:
+
+       Updated README to note new RT2 Errata list
+       Bumped version to 1.3.103
+       Added comments in ShowTransaction to let you see exact transaction / ticket Ids
+       if you view source.
+       
+2001-06-18 15:32  jesse
+
+       * bin/rt:
+
+       Better fomating for cli queue listing.
+       
+2001-06-17 16:45  jesse
+
+       * lib/RT/: Transaction.pm, User.pm:
+
+       Added PurgeTransaction datatype for Transactions.
+       
+       Removed debugging code from User.pm
+       
+2001-06-17 14:36  jesse
+
+       * webrt/Ticket/Elements/Tabs:
+
+       Fix for webui error when displaying next/prev tabs on new tix.
+       
+2001-06-15 22:23  jesse
+
+       * Makefile, README:
+
+       Bumped the version to a private pre-test 1.3.103
+       
+2001-06-15 22:19  jesse
+
+       * README, lib/RT/Ticket.pm, lib/RT/User.pm:
+
+       Debugging fixes for import oddness folks are having
+       
+2001-06-15 01:13  jesse
+
+       * webrt/Search/Bulk.html:
+
+       removed debugging output from bulk.html
+       
+2001-06-14 15:53  jesse
+
+       * Makefile, lib/RT/Transactions.pm,
+       webrt/Ticket/Elements/AddWatchers:
+
+       Installation path of man pages is now configurable
+       By default Transactions should now be ordered by date, no matter what their id sequence is.
+       Fixed an html typo in Tickets/Elements/AddWatchers
+       
+       Bumped version to 1.3.102
+       
+2001-06-14 03:21  jesse
+
+       * Makefile:
+
+       Bumped the version to 1.3.101
+       
+2001-06-14 02:47  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/Interface/Web.pm, webrt/Elements/Tabs,
+       webrt/Ticket/Elements/Tabs:
+
+       Comments in Ticket.pm about less than optimal design
+       Enhancments to Interface/Web for the bulk ticket manipulation tool.
+       Search navigation was cleaned up a bunch.
+       
+2001-06-14 01:52  jesse
+
+       * webrt/Search/Bulk.html:
+
+       file Bulk.html was initially added on branch rt-1-1.
+       
+2001-06-14 01:52  jesse
+
+       * webrt/Search/: BuildSearch, Bulk.html, Listing.html, QueueFooter,
+       QueueHeader, QueueItem:
+
+       Be vewwy vewwy quiet. I'm hunting cweeping features.
+       Like the brand new Bulk Ticket manipulator
+       
+       Removed some cruft from the repository. (unused files from tobix' first
+       search implementation)
+       
+2001-06-13 03:53  jesse
+
+       * Makefile:
+
+       Bumped version to 1.3.100
+       
+2001-06-13 03:52  jesse
+
+       * bin/mason_handler.fcgi:
+
+       The fastcgi handler works for everything except attachment viewing.  I'm going to have to think a bit about how to set
+       content type properly in a nice handler-agnostic manner.
+       
+2001-06-13 03:51  jesse
+
+       * bin/rt-mailgate:
+
+       Fixed a warn -> warning in mailgate's logging
+       
+2001-06-13 03:50  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Adjusting spacing
+       
+2001-06-13 03:50  jesse
+
+       * webrt/Ticket/Elements/Tabs:
+
+       
+       Added an error check to the new navigation bars to deal with a possible lack of active search
+       
+2001-06-12 21:28  jesse
+
+       * etc/config.pm:
+
+       Clarified required permissions for the RT Logdir
+       
+2001-06-12 19:12  jesse
+
+       * etc/config.pm, lib/RT/Queue.pm, lib/RT/Transaction.pm:
+
+       Cleaned up some docs in config.pm and Queue.pm
+       Updated the Transaction Descriptions in transaction.pm to make sure
+       that the Actor is listed.
+       
+2001-06-12 18:08  jesse
+
+       * webrt/Ticket/Elements/Tabs:
+
+       Added links to navigate within an existing search.
+       
+2001-06-08 23:57  jesse
+
+       * webrt/Ticket/Elements/EditKeywordSelects:
+
+       Made "(empty)" the same between "EditKeywordSelects" and "SearchByKeywordSelect"
+       
+2001-06-08 23:51  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Ability to set owner by name when creating tickets.
+       Ability to set timeleft when creating tickets
+       
+2001-06-08 23:00  jesse
+
+       * Makefile, README, etc/schema.Pg, lib/RT/Ticket.pm:
+
+       Added an index to etc/schema.Pg
+       Ticket.pm->Create now passes through a few more values that were missing.
+       
+       Added doc on imports w/ postgres to the readme.
+       
+       bumped the version to 1.3.99
+       
+2001-06-08 18:32  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       Fix for merging more than one ticket.
+       Fix for req2rt which imported transactions in reverse order.
+       
+2001-06-07 16:02  jesse
+
+       * Makefile, lib/RT/Queue.pm, tools/import-1.0-to-2.0:
+
+       Catch a possibly undefined value in Queue::AddWatcher
+       Bumped verion to 1.3.97
+       another attempt at fixing the owner change generation code in import.
+       
+2001-06-07 14:59  jesse
+
+       * tools/import-1.0-to-2.0:
+
+       The importer now imports the root user and the general queue.
+       
+2001-06-07 12:44  jesse
+
+       * Makefile, etc/config.pm, lib/RT/Interface/Web.pm:
+
+       Support for a seperate local component root for local WebRT mods and additions.
+       
+2001-06-06 17:25  jesse
+
+       * README:
+
+       Added instructions for migrating  1.0 instances to 2.0
+       
+2001-06-06 16:15  jesse
+
+       * Makefile:
+
+       Bumped version to 1.3.95
+       
+2001-06-06 16:15  jesse
+
+       * tools/import-1.0-to-2.0:
+
+       Importer should now import tickets created by the req importer
+       
+2001-06-06 16:12  jesse
+
+       * bin/initacls.Pg:
+
+       initacls.Pg should now handle pg database on remote machine
+       
+2001-06-06 16:05  jesse
+
+       * webrt/Ticket/Elements/ShowRequestor:
+
+       Don't show the 'About this Requestor' box if the user is privileged.
+       
+2001-06-06 15:37  jesse
+
+       * lib/RT/Date.pm:
+
+       Now date comparisons against never dtrt.
+       
+2001-06-06 15:30  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Search by content now works on the web
+       
+2001-06-06 14:22  jesse
+
+       * webrt/Admin/Groups/Members.html:
+
+       Fix for #552: spurious warning when going into group membership editor
+       
+2001-06-06 14:14  jesse
+
+       * webrt/Ticket/Elements/EditKeywordSelects:
+
+       keywordSelect selction fix (fsck #555)
+       
+2001-06-06 10:57  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       Removed spurious 'u' from line 1 of importer.
+       bumped version to 1.3.95-test2
+       
+2001-06-06 09:39  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       Possible fix for jens' user import troubles.
+       bounced version to 1.3.95-test1
+       
+2001-06-06 02:12  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       Importer wasn't properly grabbing ACLs. Bumped version to 1.3.94
+       
+2001-06-06 01:33  jesse
+
+       * tools/import-1.0-to-2.0:
+
+       Fixed a bug in the 'Make users superusers' code in the importer
+       
+2001-06-06 00:41  jesse
+
+       * lib/RT/Ticket.pm:
+
+       fixed a missing =cut (end of perldoc in ticket.pm
+       
+2001-06-06 00:40  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       fixing a typo in importer's mime parser
+       
+2001-06-06 00:19  jesse
+
+       * Makefile, lib/Makefile.PL, lib/RT/Action/Notify.pm,
+       lib/RT/Action/SendEmail.pm, tools/testdeps:
+
+       Cleaned up some dependencies
+       RT should no longer send mail when there are no recipients. thanks to the nice folks at Sensarray.com for some good debugging.
+       
+2001-06-05 23:16  jesse
+
+       * Makefile, bin/webmux.pl, tools/import-1.0-to-2.0:
+
+       Fixed some bogus header parsing in import.
+       web mux now tries to chown files again. (christian's going to test it again ;)
+       Bumped to 1.3.91 for a release
+       
+2001-06-05 11:34  jesse
+
+       * webrt/Admin/Users/Modify.html:
+
+       Rephrased the "Privileged" checkbox in AdminUsers.
+       
+2001-06-05 11:18  jesse
+
+       * Makefile, bin/webmux.pl, tools/import-1.0-to-2.0,
+       webrt/NoAuth/Logout.html:
+
+       webmux no longer tries to chown its data directories on startup
+       import now handles final_priority
+       Logout.html now refreshes to a login page
+       
+2001-06-05 10:42  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       When we added support for 'priority' transactions, we accidentally broke
+       support for 'subject' transactions.  fixed now.
+       
+2001-06-05 03:25  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       Import tool now handles priority changes.
+       
+2001-06-05 02:56  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       Minor tweak to importer to try even harder to find users in the database before creating new ones.
+       
+2001-06-05 02:32  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       importer is now less likely to try to greate nonexitent empty users.
+       
+       Makefile now chgrps web ui datafiles
+       
+2001-06-05 00:06  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       Removed out of date comments in the importer
+       Made the importer grab its libraries from the makefile. (Turned off testing code)
+       
+2001-06-05 00:01  jesse
+
+       * Makefile:
+
+       Bumped the version # to 1.3.84 for immediate release
+       
+2001-06-04 23:50  jesse
+
+       * Makefile, lib/RT/Ticket.pm, tools/import-1.0-to-2.0:
+
+       Cleanups to Ticket->Import
+       import-1.0-to-2.0 now deals with dates right.
+       
+2001-06-04 20:52  jesse
+
+       * lib/RT/Link.pm, lib/RT/Ticket.pm, lib/RT/User.pm,
+       tools/import-1.0-to-2.0:
+
+       Cleanup to Ticket, Link and User.
+       
+       merge now works on import.
+       
+       most everything in import other than ticket dates should work now.
+       
+2001-06-04 01:34  jesse
+
+       * tools/import-1.0-to-2.0:
+
+       more cleanup
+       
+2001-06-04 00:03  jesse
+
+       * tools/import-1.0-to-2.0:
+
+       First cut at importing queue adminccs and acls
+       
+2001-06-03 22:16  jesse
+
+       * tools/import-1.0-to-2.0:
+
+       Cleanup and better progress indication
+       
+2001-06-03 13:33  jesse
+
+       * lib/RT/Record.pm, lib/RT/Ticket.pm, tools/import-1.0-to-2.0:
+
+       More work on the importer. now it gets transaction creators right and catches requestor email addreses.
+       
+2001-06-03 03:43  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/Transaction.pm, tools/import-1.0-to-2.0:
+
+       Transaction.pm now allows the caller to turn off scrips (for import)
+       Ticket.pm now has a mostly working import method.
+       import-1.0-to-2.0 seems to have some basic functionality.  it's not _working_ yet, but it
+       will do most of what we want it to.
+       
+2001-06-02 20:36  jesse
+
+       * webrt/Ticket/Display.html:
+
+       Fix for Ticket #537 Crash when creating ticket without permission to view ticket
+       
+2001-06-02 14:22  jesse
+
+       * tools/import-1.0-to-2.0:
+
+       added a couple of todos to the import tool
+       
+2001-06-02 14:14  jesse
+
+       * Makefile, tools/import-1.0-to-2.0:
+
+       Work on the importer. it now runs.  (note that it probably won't yet _import_ anything)
+       
+2001-06-02 12:58  jesse
+
+       * Makefile, bin/webmux.pl, lib/RT/Ticket.pm, lib/RT/Transaction.pm:
+
+       Bumped the version to 1.3.84
+       
+       Once again chown mason data directory in webmux.pl, this time
+       using more reliable, dynamic userids, rather than compiled in defaults.
+       
+       Added an 'import' method to Ticket, for the RT1 importer.
+       
+       Added a Subject method to transaction, to simplify the lives of template authors
+       
+2001-06-01 23:38  jesse
+
+       * tools/: import-1.0-to-2.0, insertdata:
+
+       Cleaned up some names and descriptions in insertdata
+       
+       First checkin of outline of code of import tool. It's never been run.
+       It's incomplete.
+       
+2001-06-01 23:38  jesse
+
+       * tools/import-1.0-to-2.0:
+
+       file import-1.0-to-2.0 was initially added on branch rt-1-1.
+       
+2001-05-31 23:27  jesse
+
+       * lib/RT/Tickets.pm:
+
+       Cleaned up Tickets->LimitQueue a bit
+       
+2001-05-31 21:22  jesse
+
+       * Makefile, tools/testdeps:
+
+       Fixed testdeps to use new searchbuilder. bumped version to 1.3.83
+       
+2001-05-31 21:14  jesse
+
+       * lib/RT/Tickets.pm, lib/RT/Interface/Web.pm,
+       webrt/Elements/SelectKeyword:
+
+       Logic fixes for keyword select negation
+       
+2001-05-31 20:30  jesse
+
+       * lib/RT/Tickets.pm, lib/RT/Interface/Web.pm,
+       webrt/Elements/SelectKeyword:
+
+       Added support for "Tickets without keyword 'foo'.
+       
+2001-05-31 17:32  jesse
+
+       * bin/webmux.pl, lib/RT/Interface/Web.pm,
+       webrt/Ticket/Attachment/dhandler:
+
+       Fix for #522: attachments without names got short shrift.
+       webRT now dies rather than run when users can't log in due to a permissions
+       bug.
+       
+2001-05-31 02:57  jesse
+
+       * etc/: schema.Pg, schema.mysql:
+
+       Cleaned up the table indices a bit. for mysql, yanked duplicate indices.
+       for Pg, added an index (we need to regen the schema for pg soon)
+       
+2001-05-31 02:46  jesse
+
+       * Makefile, lib/RT/Interface/Web.pm, webrt/Ticket/ModifyAll.html,
+       webrt/Ticket/ModifyPeople.html, webrt/Ticket/Elements/AddWatchers,
+       webrt/Ticket/Elements/EditPeople:
+
+       You can now add watchers by email address
+       Bumped version to 1.3.82
+       
+2001-05-29 15:58  jesse
+
+       * webrt/Ticket/Attachment/dhandler:
+
+       Fixed the attachments display bug.
+       
+2001-05-29 15:48  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Fix for #513 - status update without permissions returns no error message
+       
+2001-05-29 00:38  jesse
+
+       * Makefile:
+
+       Bumped version to 1.3.81
+       
+2001-05-28 20:39  jesse
+
+       * bin/webmux.pl:
+
+       Removed a bogus line from webmux
+       
+2001-05-28 20:29  jesse
+
+       * lib/RT/: Ticket.pm, Interface/Web.pm:
+
+       Work to solve 325: updating detritus in update listing
+       
+2001-05-28 17:35  jesse
+
+       * bin/rt, etc/schema.mysql, etc/schema.pm, lib/RT/Ticket.pm,
+       lib/RT/Interface/CLI.pm, webrt/Ticket/Attachment/dhandler:
+
+       Work on the CLI to resolve #482.  Can't add content when creating tix with the CLI
+       Do a bunch of better error checking on ticket creation.
+       
+       Generalized the ticket status checking code.
+       
+       Added another index on Attachments for increased speed.
+       
+2001-05-28 15:49  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Removed code which auto-opened tickets that were new.  It should
+       be a scrip and it's too late in teh release cycle to do this right now.
+       
+2001-05-28 15:24  jesse
+
+       * webrt/SelfService/: Details.html, Elements/MyRequests:
+
+       Fixed #506. Non-priv user can't see tickets
+       
+2001-05-23 23:53  jesse
+
+       * webrt/SelfService/Elements/MyRequests:
+
+       Fixed MyRequest so it shows your tickets.
+       
+2001-05-23 23:18  jesse
+
+       * Makefile:
+
+       Bumped the version to 1.3.80
+       
+2001-05-23 23:01  jesse
+
+       * bin/webmux.pl, docs/manual.pod, webrt/Elements/MyRequests,
+       webrt/Elements/MyTickets, webrt/NoAuth/Logout.html,
+       webrt/Ticket/ModifyAll.html:
+
+       Fixed logout bug from 1.3.79 http://fsck.com/rt2/Ticket/Display.html?id=500
+       Fixed bug in jumbo from 1.3.79 http://fsck.com/rt2/Ticket/Display.html?id=496
+       Added a bit of docs about templates to the manual
+       
+       Cleaned up the ui for MyRequests and MyTickets a bit.
+       
+2001-05-23 12:34  jesse
+
+       * webrt/SelfService/: Prefs.html, index.html:
+
+       Little bit of cleanup to SelfService. added prefs.
+       
+2001-05-23 12:34  jesse
+
+       * webrt/SelfService/Prefs.html:
+
+       file Prefs.html was initially added on branch rt-1-1.
+       
+2001-05-23 12:07  jesse
+
+       * Makefile:
+
+       Bumped version to 1.3.79.
+       
+2001-05-23 12:06  jesse
+
+       * README, tools/testdeps:
+
+       Added dependecy on DBIx::SearchBuilder 0.34
+       
+2001-05-23 11:43  jesse
+
+       * bin/: rt-mailgate, webmux.pl:
+
+       Made sure that the webui always writes sessions to disk
+       Fixed a typo in the mail gateway.
+       
+2001-05-23 00:09  jesse
+
+       * Makefile, bin/rt-mailgate:
+
+       Added a bit of code to rt-mailgate to deal with not properly loading users on
+       ticket creation.
+       
+2001-05-22 23:01  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Within Ticket->Create, always get a Queue object as SystemUser for like, defaults and stuff.
+       
+2001-05-21 18:12  jesse
+
+       * etc/config.pm:
+
+       Added comments from Feargal about how to configure RT for http urls.
+       
+2001-05-21 16:37  jesse
+
+       * Makefile:
+
+       bumped version to 1.3.78
+       
+2001-05-21 16:36  jesse
+
+       * lib/RT/Ticket.pm, webrt/Ticket/Display.html:
+
+       Tickets now open when they're new and get acted on.
+       
+2001-05-21 16:09  jesse
+
+       * lib/RT/Action/: Autoreply.pm, SendEmail.pm:
+
+       Fix for Ticket #481 $CorrespondAddress= variable in config.pm doesn't work
+       
+2001-05-21 15:54  jesse
+
+       * webrt/Elements/MessageBox:
+
+       Added a space at teh end of the line, per the standard convention for signature
+       dashes.
+       
+2001-05-21 15:54  jesse
+
+       * webrt/Elements/MessageBox:
+
+       If the user doesn't have a signature, don't include signature dashes
+       
+2001-05-21 15:17  jesse
+
+       * lib/RT/Tickets.pm, webrt/Ticket/ModifyAll.html:
+
+       Dead tickets aren't searchable via the ui anymore.
+       ModifyAll is a bit smarter about what to let people do.
+       
+2001-05-21 14:02  jesse
+
+       * lib/RT/Keyword.pm, webrt/Admin/Keywords/index.html:
+
+       Editing keywords without permission now actually tells you so.
+       
+2001-05-21 13:32  jesse
+
+       * webrt/Admin/: Global/Scrips.html, Queues/Scrips.html:
+
+       Users without permission to delete scrips now get a proper permission denied.
+       
+2001-05-21 13:20  jesse
+
+       * webrt/Ticket/: Update.html, Elements/Tabs:
+
+       The ticket update form is now a little smarter about only letting people perform
+       updates that they have the right to perform.
+       
+2001-05-17 23:09  jesse
+
+       * Makefile:
+
+       Chmod the makefile in the make dist procedure.
+       
+2001-05-17 23:05  jesse
+
+       * README:
+
+       Cleaned up some instructions. Thanks Feargal.
+       
+2001-05-17 22:50  jesse
+
+       * Makefile, lib/RT/Transaction.pm, lib/RT/Action/SendEmail.pm,
+       tools/insertdata:
+
+       Fixed scrip actions to activate when there's a missing scripcontent.
+       
+       Added a Content method to Transaction.pm
+       Fixed Tempates to use new Content method.
+       
+2001-05-17 16:19  jesse
+
+       * lib/RT/Transaction.pm, lib/RT/Action/SendEmail.pm,
+       webrt/Ticket/Display.html, webrt/Ticket/Update.html:
+
+       Better debugging info for some scrips.
+       
+       Fixed bugs which prevented status and owner to change on correspondence
+       and comment.
+       
+2001-05-17 12:36  jesse
+
+       * Makefile, README:
+
+       Bumped the version. Added Mark Vevers to the thansk in the readme.
+       
+2001-05-17 12:34  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Removed some obsolete code. fixed a bug in Comment recording from Mark Vevers
+       
+2001-05-16 18:51  jesse
+
+       * Makefile, tools/testdeps:
+
+       Fixed testdeps to not install bogus new DBD::mysql rev.
+       Bumped rev to 1.3.75
+       
+2001-05-16 17:30  jesse
+
+       * Makefile:
+
+       bumped to 1.3.74
+       
+2001-05-16 17:30  jesse
+
+       * webrt/Admin/Groups/Modify.html:
+
+       Fixed a small bug in Groups editing
+       
+2001-05-16 17:21  jesse
+
+       * tools/testdeps:
+
+       Bumped version to 1.3.73.
+       Bumped dependency on searchbuilder to 0.33
+       
+2001-05-16 17:15  jesse
+
+       * webrt/: Elements/TitleBoxStart, Ticket/Update.html,
+       Ticket/Elements/Tabs:
+
+       The quick link for 'Resolve' now requests that the user enter a ticket update
+       
+2001-05-16 15:45  jesse
+
+       * README, lib/RT/Interface/Web.pm, tools/testdeps,
+       webrt/Ticket/Display.html:
+
+       Added some documentation about what to do if the install fails.
+       
+       Specified a minimum working version of DBD::mysql.
+       
+       Fixed support for entering updates via the webui.
+       
+2001-05-15 00:55  jesse
+
+       * lib/RT/Action/Autoreply.pm:
+
+       Fixed a typo that broke autoreplies
+       
+2001-05-15 00:37  jesse
+
+       * tools/testdeps:
+
+       added Errno dependency
+       
+2001-05-14 23:44  jesse
+
+       * etc/config.pm, tools/insertdata, webrt/Admin/Queues/People.html:
+
+       Cleaned up a bit of phrasing. cleaned up some extra /s in urls.
+       
+2001-05-14 22:49  jesse
+
+       * Makefile, bin/mason_handler.fcgi, bin/mason_handler.scgi, bin/rt,
+       bin/webmux.pl, lib/Makefile.PL, lib/RT/Date.pm,
+       lib/RT/Interface/Web.pm, tools/testdeps:
+
+       Switched from Date::Manip To Graham Barr's Date::Parse
+       
+2001-05-14 18:58  jesse
+
+       * lib/RT/Scrips.pm, lib/RT/Ticket.pm, lib/RT/Interface/Web.pm,
+       webrt/Ticket/Display.html, webrt/Ticket/Elements/Tabs:
+
+       Work on the web frontend. the "quick" links have been cleaned up and decomplexified.
+       
+2001-05-14 17:11  jesse
+
+       * lib/RT/User.pm, lib/RT/Interface/Web.pm,
+       webrt/Admin/Groups/Modify.html, webrt/Admin/Users/Modify.html:
+
+       Cleaned up bugs related to spurious extra display of information in user and group modification and ACL deletion
+       when the user didn't have the right to do so.
+       
+2001-05-13 23:05  jesse
+
+       * webrt/Admin/Users/index.html:
+
+       no longer provide a link to create a new user to someone who doesn't have rights to do so.
+       
+2001-05-13 22:49  jesse
+
+       * lib/RT/Ticket.pm, tools/insertdata, webrt/Ticket/Modify.html:
+
+       yanked bogus Ticket Queue caching
+       rephrased ticket creation autoreply
+       
+2001-05-13 22:15  jesse
+
+       * webrt/User/Prefs.html:
+
+       Added a hack to make sure that session gets written to disk
+       
+2001-05-13 22:11  jesse
+
+       * webrt/User/Prefs.html:
+
+       more work on letting users edit their own passwords
+       
+2001-05-13 21:40  jesse
+
+       * webrt/Ticket/: Attachment/dhandler, Elements/ShowTransaction:
+
+       Fix for #433. now attachements are displayable after merges.
+       
+2001-05-12 17:46  jesse
+
+       * Makefile, lib/Makefile.PL, tools/testdeps, webrt/User/Prefs.html:
+
+       Started work on user prefs. password changing is untested
+       
+       Fixed dependency on SB to .31 and bumped version
+       
+2001-05-11 12:32  jesse
+
+       * lib/RT/Record.pm:
+
+       turned on use of DBIx::SearchBuilder::Record::Cachable
+       
+2001-05-11 12:30  jesse
+
+       * lib/RT/User.pm, webrt/Elements/Quicksearch:
+
+       Redid how the ACL checking works for great perf gains.
+       Added stalled lists to quicksearch.
+       
+2001-05-09 20:11  jesse
+
+       * Makefile:
+
+       Bumping the version # so people working off cvs get less confused.
+       
+2001-05-09 20:11  jesse
+
+       * bin/rt-mailgate, lib/RT/Queue.pm, lib/RT/Action/Autoreply.pm,
+       lib/RT/Action/Notify.pm, lib/RT/Action/NotifyAsComment.pm,
+       lib/RT/Action/SendEmail.pm:
+
+       Cleanup in the mail sending stuff.
+               fix for ticket 403:  RT should no longer send mail when it has
+               no recipients
+       
+               fix for #404: Squelch-Replies-To should be a bit more intelligent,
+               since it was rewritten from scratch
+       
+               RT should now properly send mail to queue ccs and queue admin ccs
+               when you ask it to
+       
+               internally, RT's mail sending stuff now lets you pass around
+               arrays of addresses to send mail to, rather than comma delimited
+               strings. More flexible down the line.
+       
+2001-05-09 16:39  jesse
+
+       * README:
+
+       Updated the readme to note the dependency on setuid perl
+       
+2001-05-09 16:08  jesse
+
+       * webrt/Ticket/Elements/ShowLinks:
+
+       fix for RT/fsck.com: Ticket #440 [rt-devel] 'Depended on by' doesn't work
+       
+2001-05-08 20:48  jesse
+
+       * etc/schema.mysql, etc/schema.pm, lib/RT/Ticket.pm,
+       lib/RT/Transaction.pm, webrt/Admin/Elements/UserTabs,
+       webrt/Admin/Queues/People.html, webrt/Elements/MyRequests,
+       webrt/Elements/MyTickets, webrt/Elements/Tabs,
+       webrt/Ticket/Elements/ShowBasics,
+       webrt/Ticket/Elements/ShowRequestor:
+
+       Schema.pm now has a few more indices. the mysql schema has been regenerated, but not
+       the Pg or oracle schemas.
+       
+       in Ticket.pm, a message got the queue id replaced with the queue name
+       
+       removed a spurious variable localization in Transaction.pm
+       
+       Removed a pointer to a nonexistent page  on admin/user/modify
+       
+       Modifying queue watchers now has a more coherent title
+       
+       "This user's tickets, My requests and My tickets are now limited to the 25 highest priority items
+       attached to you.
+       
+       The "Administration" tab has been changed to the "Configuration" tab
+       
+2001-04-04 16:55  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Fixing a comment in the web interface. no functional changes.
+       
+2001-04-04 16:52  jesse
+
+       * Makefile:
+
+       
+       Bumping the version to 1.3.70 for release
+       
+2001-04-04 16:51  jesse
+
+       * lib/RT/User.pm:
+
+       lib/RT/User no longer treats "ModifySelf" as equivalent to "AdminUsers" for the current user.
+       
+2001-04-04 15:08  jesse
+
+       * lib/RT/KeywordSelect.pm, lib/RT/Action/Autoreply.pm,
+       webrt/Admin/Global/Keywords.html, webrt/Admin/Queues/Keywords.html:
+
+       Fixed a crashing bug and an ACL violation on keywordselect deletion
+       
+2001-04-04 00:19  jesse
+
+       * Makefile:
+
+       bumping version for release
+       
+2001-04-03 21:14  jesse
+
+       * Makefile:
+
+       Bumped the version
+       
+2001-04-03 21:08  jesse
+
+       * etc/config.pm, lib/RT/Action/Notify.pm:
+
+       Added configuration tweak to not set a :; To header if the user doesn't want it.
+       
+2001-04-03 17:32  jesse
+
+       * lib/RT/Action/Notify.pm:
+
+       Fix for sendmail null list syntax.  : ;  is not the same as :; to sendmail
+       
+2001-04-03 17:05  jesse
+
+       * webrt/Admin/Queues/: GroupRights.html, UserRights.html:
+
+       removed a couple of stray 's in the html
+       
+2001-04-03 16:50  jesse
+
+       * webrt/Elements/Header:
+
+       Dropped doctype back to 4.0 because 4.01 makes mozilla render things funny
+       
+2001-04-03 16:47  jesse
+
+       * webrt/Elements/Quicksearch:
+
+       <TABLE>
+       should have been </TABLE>
+       
+2001-04-03 16:30  jesse
+
+       * webrt/Elements/Quicksearch:
+
+       Added a feature request/patch from nick@netability.ie
+       http://fsck.com/rt2/Ticket/Display.html?id=345
+       
+2001-04-03 16:01  jesse
+
+       * tools/testdeps:
+
+       Adding in testing for params::validate 0.02
+       
+2001-04-03 15:46  jesse
+
+       * Makefile:
+
+       Bumped the version slightly for a prerelease
+       
+2001-04-03 15:40  jesse
+
+       * lib/RT/Action/Autoreply.pm, lib/RT/Action/SendEmail.pm,
+       tools/insertdata:
+
+       Fixed autoreply by adding a new scrip. some misc. cleanup in SendEmail (method calls were on the wrong object)
+       
+2001-04-03 15:40  jesse
+
+       * lib/RT/Action/Autoreply.pm:
+
+       file Autoreply.pm was initially added on branch rt-1-1.
+       
+2001-04-03 15:15  jesse
+
+       * lib/RT/Action/SendEmail.pm:
+
+       Fixed a bug in the 'blacklist' regexp
+       
+2001-04-03 15:04  jesse
+
+       * etc/config.pm, lib/RT/Action/SendEmail.pm,
+       lib/RT/Interface/Web.pm, webrt/Ticket/Elements/ShowRequestor:
+
+       Two seperate fixes for the "RT doesn't send mail" problem. One that helps fix Mail::Internet->send
+       and one that provides an alternative.
+       
+       A fix for a bad link in ShowRequestor.
+       
+       Updating fields via the web ui should now be a bit more descriptive
+       
+2001-04-03 02:31  jesse
+
+       * webrt/Ticket/ModifyPeople.html:
+
+       Missed one of the mason fixes
+       
+2001-04-03 02:31  jesse
+
+       * Makefile, bin/rt-mailgate, etc/config.pm, lib/RT/Date.pm,
+       lib/RT/Ticket.pm, lib/RT/Transaction.pm, lib/RT/Watcher.pm,
+       lib/RT/Interface/Web.pm, webrt/Admin/Global/GroupRights.html,
+       webrt/Admin/Global/Keywords.html, webrt/Admin/Global/Scrips.html,
+       webrt/Admin/Global/UserRights.html,
+       webrt/Admin/Keywords/index.html,
+       webrt/Admin/Queues/GroupRights.html,
+       webrt/Admin/Queues/Keywords.html, webrt/Admin/Queues/People.html,
+       webrt/Admin/Queues/Scrips.html, webrt/Admin/Queues/UserRights.html,
+       webrt/Ticket/Modify.html, webrt/Ticket/ModifyAll.html,
+       webrt/Ticket/ModifyDates.html, webrt/Ticket/ModifyPeople.html:
+
+       Added support for parsing Cc and To in the mail gateway
+       (resolves the last work item for beta 2 - #219)
+       
+       Ticket due date is no longer set to "right now" if the queue has an
+       undefined "default due in"
+       
+       Cleanup warnings in date, ticket and watcher
+       
+       fixes for a bunch of mason 1.01 related bugs (Mason changed handling of combined GET and POST, thus breaking RT)
+       
+2001-04-02 23:15  jesse
+
+       * README, bin/webmux.pl:
+
+       Cleanup for mason 1.01.
+       Moved Apache::DBI out of the webmux into the instructions
+       
+2001-04-02 17:56  jesse
+
+       * tools/initdb:
+
+       Typo fix from  "Nick Hilliard" <nick@netability.ie>
+       
+2001-04-02 15:32  jesse
+
+       * Makefile, lib/RT/Keyword.pm, webrt/NoAuth/Logout.html:
+
+       Makefile cleanup.
+       Fix for #342 - reported by ivan
+       
+       Fix for a bug that clobbered logouts.
+       
+2001-04-02 03:09  jesse
+
+       * Makefile:
+
+       Bumped version for release
+       
+2001-04-01 19:19  jesse
+
+       * bin/rtadmin, lib/RT/ACE.pm, lib/RT/Interface/Web.pm,
+       webrt/Admin/Elements/SelectRights,
+       webrt/Admin/Global/GroupRights.html,
+       webrt/Admin/Global/UserRights.html,
+       webrt/Admin/Queues/GroupRights.html,
+       webrt/Admin/Queues/UserRights.html:
+
+       Redid the ACL editor so that it's much much faster. and easier to use...if not quite as pretty.
+       
+2001-04-01 17:31  jesse
+
+       * webrt/Ticket/Elements/: ShowDates, ShowRequestor:
+
+       fixing a couple of html typos
+       
+2001-03-31 03:07  jesse
+
+       * Makefile:
+
+       fixing Changelog generation
+       
+2001-03-31 02:52  jesse
+
+       * Makefile:
+
+       Bumped the version.
+       
+2001-03-31 02:52  jesse
+
+       * webrt/User/Prefs.html:
+
+       Cleaned up user preferences somewhat. removed things that don't do anything.
+       
+               -j
+       
+2001-03-31 01:59  jesse
+
+       * bin/rt:
+
+       Fix for #88   RT now lets you use the CLI to search for tickets, based on links to other tickets
+       
+2001-03-31 01:20  jesse
+
+       * README:
+
+       Fix for #216: Web session files need better handling and cleanup
+       
+2001-03-31 00:49  jesse
+
+       * Makefile, README, bin/rtadmin, bin/webmux.pl, etc/config.pm:
+
+       Moved RT Session data out of /tmp to somewhere that makes more sense.
+       This is the first half of the fix for #216.  Now we just need to document the cronjob reaper to kill old sessions
+       
+       Cleaned up creation of queues and users in bin/rtadmin
+       
+       Updates to the README
+       
+2001-03-30 23:44  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Fix for http://fsck.com/rt2/Ticket/Display.html?id=324
+       mail from requestors does not reopen ticket
+       
+2001-03-30 23:19  jesse
+
+       * bin/rtadmin, lib/RT/Queue.pm, lib/RT/Watcher.pm:
+
+       Work on Watchers and editing watchers from the CLI tool.  added support for --list-{queues|users|groups}
+       
+2001-03-30 19:31  jesse
+
+       * bin/rt, lib/RT/ACE.pm, lib/RT/Queue.pm, lib/RT/Ticket.pm,
+       lib/RT/Tickets.pm, lib/RT/Watcher.pm, lib/RT/Action/SendEmail.pm,
+       lib/RT/Interface/Web.pm:
+
+       A bunch of reworking of access control for watchers. should be much more robust
+       and more flexible.
+       
+       Resolves http://fsck.com/rt2/Ticket/Display.html?id=253 by adding the "Watch" and "WatchAsAdminCc" rights.
+       
+2001-03-30 15:41  jesse
+
+       * lib/RT/: Action/Notify.pm, Interface/Web.pm:
+
+       Tiny cleanup in Interface/Web.pm.  no functional difference there just being more explicit.
+       
+       Fix for #292: Sender of a message is no longer notified of that message.
+       
+2001-03-30 02:55  jesse
+
+       * Makefile:
+
+       Bumped the version
+       
+2001-03-30 02:37  jesse
+
+       * webrt/Elements/SelectDate:
+
+       Fixed an as-yet-untickled bug in SelectDate that incremented things too much
+       
+2001-03-30 02:32  jesse
+
+       * lib/RT/Template.pm, webrt/Admin/Global/Scrips.html:
+
+       Fixes to template creation. stupid typo.
+       Global scrips creation should now work better
+       
+2001-03-30 02:12  jesse
+
+       * webrt/: Admin/Elements/GroupTabs, Admin/Groups/Members.html,
+       Admin/Groups/Modify.html, Admin/Users/Modify.html,
+       Elements/ListActions:
+
+       Fix for PseudoGroups having a prompt to add members.
+       some cleanups to not make the "Results" box pop up when it's not wanted.
+       
+2001-03-30 01:41  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Attached is a patch to implement the ShowTicketComments in the
+       display of transactions of a ticket.
+       
+                       Arthur de Jong <arthur@West.NL>
+       
+2001-03-30 01:34  jesse
+
+       * lib/RT/Scrip.pm, webrt/Admin/Global/Scrips.html,
+       webrt/Admin/Queues/Scrips.html:
+
+       Fix for 328: Re: [rt-users] 1.3.64 & postgres & scrips
+       there was an ACL bug.
+       
+2001-03-28 22:15  jesse
+
+       * Makefile, webrt/autohandler:
+
+       Fixed a tiny bug in the new authandler that caused logout to fail
+       
+2001-03-27 22:30  jesse
+
+       * webrt/index.html:
+
+       fixed index.html
+       
+2001-03-27 22:26  jesse
+
+       * Makefile, webrt/Admin/Groups/Members.html,
+       webrt/Ticket/Update.html:
+
+       Added a couple of fixes from Arthur at west.nl
+       Groups/Members deals better with multiple selections
+       Ticket/Update now displays queue watchers.
+       bumped the version to 1.3.63
+       
+2001-03-27 04:25  jesse
+
+       * docs/manual.pod, webrt/autohandler, webrt/index.html,
+       webrt/Admin/Users/index.html, webrt/Elements/ShadedBox,
+       webrt/Search/Listing.html:
+
+       Lots of cleanup of the auothandler.
+       a tiny bit of doc about an alternative apache configuration.
+       some web ui cleanup so that when you are on a page, your location is more properly hilighted.
+       admin/users includes some more verbiage explaining what's going on.
+       
+2001-03-25 00:44  jesse
+
+       * Makefile, lib/RT/Interface/Web.pm:
+
+       Bumped the version.
+       cosmetic fix in Interface/Web.pm
+       
+2001-03-23 20:33  jesse
+
+       * lib/RT/: Action/SendEmail.pm, Interface/Web.pm:
+
+       Fixed for TEXTAREA newline bug and Correspondence subject bug.
+       
+2001-03-23 00:34  jesse
+
+       * lib/RT/Action/Notify.pm:
+
+       : goes outside the '' in undisclosed recipients
+       
+2001-03-23 00:30  jesse
+
+       * lib/RT/: ACL.pm, ObjectKeywords.pm:
+
+       Removed some very loud uneccesary debugging statements
+       
+2001-03-23 00:27  jesse
+
+       * Makefile, lib/RT/Action/Notify.pm, lib/RT/Action/SendEmail.pm:
+
+       Work on mail sending routines to be more careful about newlines in subjects.
+       And about Undisclosed recipients
+       
+2001-03-22 23:17  jesse
+
+       * lib/RT/Action/Notify.pm:
+
+       yet another typo in notify.pm
+       
+2001-03-22 23:12  jesse
+
+       * lib/RT/Action/Notify.pm:
+
+       typo fix
+       
+2001-03-22 23:09  jesse
+
+       * lib/RT/Action/: Notify.pm, SendEmail.pm:
+
+       Cleanups and fixes to Notify and SendEmail
+       
+2001-03-22 19:46  jesse
+
+       * webrt/Ticket/ModifyAll.html:
+
+       TicketObj->Ticket
+       
+2001-03-22 19:42  jesse
+
+       * lib/RT/Action/Notify.pm:
+
+       A more functional Undisclosed Recipients;
+       
+2001-03-22 19:28  jesse
+
+       * webrt/Ticket/: ModifyAll.html, Elements/Tabs:
+
+       Elements/Tabs now does better access control checking before making
+       buttons available to people.
+       
+       ModifyAll.html has a better title and should now update objectkeywords.
+       
+2001-03-22 18:18  jesse
+
+       * lib/RT/: Date.pm, Ticket.pm:
+
+       Added  a routine to Date.pm to advance the date N days
+       
+       Started using queue defaults on Ticket->Create
+       
+2001-03-22 18:17  jesse
+
+       * webrt/: autohandler, Elements/Login, NoAuth/Login.html,
+       Search/autohandler, Ticket/autohandler:
+
+       Cleanup of the login process. now you can't accidentally call "Login.html"
+       when you're already logged in.
+       removed a couple of bogus old autohandlers
+       
+2001-03-22 18:17  jesse
+
+       * webrt/Elements/Login:
+
+       file Login was initially added on branch rt-1-1.
+       
+2001-03-22 14:45  jesse
+
+       * webrt/Ticket/Elements/EditPeople:
+
+       EditPeople should now enforce a list of possible owners reasonably
+       
+2001-03-21 21:40  jesse
+
+       * Makefile:
+
+       bumped the version to .60
+       
+2001-03-21 21:22  jesse
+
+       * docs/manual.pod:
+
+       Adding some content to the manual. and even more places where the manual is
+       missing content. :/
+       
+2001-03-21 12:21  jesse
+
+       * lib/RT/Action/NotifyAsComment.pm:
+
+       NotifyAsComment should now DTRT and send mail from the comments email address
+       
+2001-03-21 02:46  jesse
+
+       * Makefile:
+
+       yanked some cruft from the ChangeLog creator
+       
+2001-03-21 02:44  jesse
+
+       * Makefile, README:
+
+       Bumped the version. added some people to the readme
+       
+2001-03-21 02:39  jesse
+
+       * webrt/Ticket/Elements/ShowBasics:
+
+       Fix for 242: time left displayed via the web ui is now more intuitive
+       
+2001-03-21 02:37  jesse
+
+       * bin/rt, bin/rt-mailgate, lib/RT.pm, lib/RT/Attachment.pm,
+       lib/RT/Date.pm, lib/RT/Tickets.pm, lib/RT/User.pm:
+
+       Added more error checking and error handling to the mail gateway
+       finished code in Attachment.pm to handle too-long attachments
+       
+       added support for limiting by date to the CLI
+       
+       fixed little bugs in user, date and tickets.
+       
+2001-03-20 19:02  jesse
+
+       * docs/manual.pod:
+
+       file manual.pod was initially added on branch rt-1-1.
+       
+2001-03-20 19:02  jesse
+
+       * docs/manual.pod:
+
+       actually adding the outline of the manual
+       
+2001-03-20 19:01  jesse
+
+       * Makefile, bin/rt, docs/API, docs/README.docs, docs/Security,
+       docs/keywords, etc/config.pm:
+
+       Starting the doc cleanup
+       
+2001-03-20 15:26  jesse
+
+       * webrt/Elements/SelectTicketSortBy:
+
+       LastUpdated, not LastUpdate
+       
+2001-03-20 15:21  jesse
+
+       * webrt/Elements/: SelectDate, SelectTicketSortBy:
+
+       more things to sort by.
+       select date shouldn't insert spurious null dates.
+       
+2001-03-20 13:27  jesse
+
+       * lib/RT/Action/SendEmail.pm:
+
+       fixed a typo. $defined is not useful perl ;)
+       
+2001-03-20 13:21  jesse
+
+       * lib/RT/Action/: Notify.pm, SendEmail.pm:
+
+       Fixed a couple bugs in Notify and SendEmail that should get mail flowing
+       for transactions without content.
+       
+2001-03-20 13:08  jesse
+
+       * webrt/Ticket/: Attachment/dhandler, Elements/ShowTransaction:
+
+       Making web based attachment display deal nicely with too-long message bodies
+       
+2001-03-20 04:26  jesse
+
+       * Makefile:
+
+       bumping the version
+       
+2001-03-20 04:20  jesse
+
+       * lib/RT/Action/SendEmail.pm:
+
+       regexps with three /s need a leading s ;)
+       
+2001-03-20 04:16  jesse
+
+       * bin/rt-mailgate, lib/RT/Template.pm:
+
+       fixing scrips-related mailing stuff. some transaction mailing used to bomb out.
+       
+2001-03-20 02:46  jesse
+
+       * Makefile, bin/rt-mailgate, etc/config.pm,
+       lib/RT/Action/SendEmail.pm, tools/insertdata:
+
+       Work on robustification of the mail gateway. including single-transaction
+       blacklisting of addresses that might generate bounces.
+       
+2001-03-20 00:41  jesse
+
+       * lib/RT/User.pm:
+
+       Work on user.pm to allow users to be disabled.
+       
+2001-03-19 18:03  jesse
+
+       * webrt/: autohandler, Elements/MyRequests, NoAuth/Login.html,
+       NoAuth/Reminder.html:
+
+       fixed the MyRequests bug that caused them not to be listed.
+       nonexistent users no longer get shunted to SelfService.
+       
+2001-03-19 15:30  jesse
+
+       * Makefile, bin/mason_handler.fcgi, bin/mason_handler.scgi, bin/rt,
+       bin/rt-mailgate, bin/rtadmin, webrt/Admin/Elements/ModifyTemplate,
+       webrt/Admin/Elements/ModifyUser, webrt/Admin/Global/Template.html,
+       webrt/Admin/Queues/Template.html, webrt/Admin/Users/Modify.html,
+       webrt/Elements/MessageBox:
+
+       Fixes for perl path and textarea bugs noted by Arthur de Jong
+       
+2001-03-18 14:58  jesse
+
+       * lib/RT/Attachment.pm:
+
+       Removed an extra signature attachment
+       
+2001-03-17 14:57  jesse
+
+       * Makefile:
+
+       bumped version
+       
+2001-03-17 14:55  jesse
+
+       * lib/Makefile.PL, lib/RT/Tickets.pm, lib/RT/Interface/Web.pm,
+       webrt/Elements/Quicksearch, webrt/Elements/SelectDate,
+       webrt/Elements/SelectDateType, webrt/Elements/TitleBoxEnd,
+       webrt/Search/PickRestriction:
+
+       Fixed a bug on simple actions reported by christian.
+       Implemented search by date via web ui
+       
+2001-03-16 03:55  jesse
+
+       * Makefile, lib/RT/Interface/Web.pm, webrt/Elements/Header:
+
+       Fixed a tiny typo that prevented objectkeyword editing
+       
+2001-03-16 02:29  jesse
+
+       * Makefile:
+
+       Bumped the version.
+       
+2001-03-16 02:27  jesse
+
+       * Makefile, README, bin/initacls.Oracle, bin/initacls.Pg,
+       bin/initacls.mysql, bin/initdb.Oracle, tools/initdb:
+
+       some clarifications to the install procedure.
+       
+2001-03-16 02:10  jesse
+
+       * lib/RT/Tickets.pm, webrt/Search/Listing.html:
+
+       Fixed #2 WebRT doesn't refresh searches properly. the oldest bug in the bug tracking
+       system ;)
+       
+       Fixed Tickets->Count
+       
+2001-03-16 01:39  jesse
+
+       * webrt/Admin/: Global/Template.html, Global/Templates.html,
+       Queues/Modify.html, Queues/Template.html, Queues/Templates.html:
+
+       now you can create templates with the web ui.
+       
+2001-03-16 00:39  jesse
+
+       * webrt/Ticket/Elements/EditLinks:
+
+       file EditLinks was initially added on branch rt-1-1.
+       
+2001-03-16 00:39  jesse
+
+       * webrt/Ticket/ModifyAll.html:
+
+       file ModifyAll.html was initially added on branch rt-1-1.
+       
+2001-03-16 00:39  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/Interface/Web.pm, webrt/Elements/Tabs,
+       webrt/Ticket/Modify.html, webrt/Ticket/ModifyAll.html,
+       webrt/Ticket/ModifyDates.html, webrt/Ticket/ModifyLinks.html,
+       webrt/Ticket/ModifyPeople.html, webrt/Ticket/ValidateUpdate.html,
+       webrt/Ticket/Elements/EditKeywordSelects,
+       webrt/Ticket/Elements/EditLinks, webrt/Ticket/Elements/EditPeople,
+       webrt/Ticket/Elements/Tabs:
+
+       A bunch of Ticket modification refactoring and cleanup.
+       Added a "ModifyAll" at sam hartman's suggestion
+       
+2001-03-14 21:20  jesse
+
+       * bin/rt:
+
+       --id=43-45 should now work. there was a regex typo
+       
+2001-03-14 04:37  jesse
+
+       * Makefile, bin/initacls.Oracle, etc/acl.Oracle, etc/acl.mysql,
+       etc/schema.Oracle, etc/user.Oracle, tools/initdb, tools/testdeps:
+
+       reworked init procedures, oracle schema and
+       acl setup for mysql and postgres and oracle.
+       
+       RT2 now runs on oracle.
+       
+2001-03-14 02:08  jesse
+
+       * etc/acl.Oracle, etc/schema.Oracle, tools/initdb:
+
+       hacking to make oracle work. we're much of the way there. next up:
+       schema updates
+       
+2001-03-14 02:08  jesse
+
+       * etc/acl.Oracle:
+
+       file acl.Oracle was initially added on branch rt-1-1.
+       
+2001-03-14 01:03  jesse
+
+       * webrt/Admin/Queues/GroupRights.html:
+
+       file GroupRights.html was initially added on branch rt-1-1.
+       
+2001-03-14 01:03  jesse
+
+       * webrt/Admin/Queues/UserRights.html:
+
+       file UserRights.html was initially added on branch rt-1-1.
+       
+2001-03-14 01:03  jesse
+
+       * Makefile, lib/RT/ACE.pm, lib/RT/Interface/Web.pm, tools/initdb,
+       webrt/Admin/Elements/QueueTabs,
+       webrt/Admin/Global/GroupRights.html, webrt/Admin/Queues/ACL.html,
+       webrt/Admin/Queues/GroupRights.html,
+       webrt/Admin/Queues/UserRights.html, webrt/Elements/Tabs,
+       webrt/NoAuth/webrt.css:
+
+       Cleanup to the ACL editors (consistency)
+       a bit of web ui tuning.
+       
+       fixes for initdb issues (thanks, jhutz)
+       
+       minor cleanups to ACE.pm
+       
+2001-03-13 22:44  jesse
+
+       * lib/RT/Transaction.pm, lib/RT/Transactions.pm, tools/testdeps:
+
+       Updated DBIx::SearchBuilder dependency
+       
+       code cleanup and sketching in Transaction.pm and Transactions.pm
+       
+2001-03-12 21:14  jesse
+
+       * Makefile, etc/schema.Pg, etc/schema.mysql, tools/initdb,
+       tools/testdeps:
+
+       initdb now caches schema, eliminating the install-time dependency on DBIx::DBSchema.
+       
+       this is 1.3.50, folks. it has what I believe to be functional postgres support
+       
+2001-03-11 21:58  jesse
+
+       * etc/config.pm, etc/schema.pm, lib/RT/Attachment.pm,
+       lib/RT/Ticket.pm, tools/initdb:
+
+       Cleanup in Ticket.pm
+       Bugfixes to Attachment.pm (for postgres mimencoding supporT)
+       
+       reconfigured to use the new DBIx::DBSchema (which isn't out yet. but should
+       be soon)
+       
+       still need to do a bit more work on attachments configuration.
+       
+2001-03-11 02:45  jesse
+
+       * bin/rt-mailgate:
+
+       file rt-mailgate was initially added on branch rt-1-1.
+       
+2001-03-11 02:45  jesse
+
+       * Makefile, README, bin/rt-mailgate, bin/rtmux.pl, bin/webmux.pl,
+       etc/config.pm, etc/schema.Oracle, etc/schema.pm, lib/RT.pm,
+       lib/RT/Attachment.pm, lib/RT/ScripAction.pm, lib/RT/Ticket.pm,
+       lib/RT/Action/SendEmail.pm, lib/RT/Interface/CLI.pm,
+       lib/RT/Interface/Email.pm, tools/insertdata:
+
+       This is a bit more of a 'comprehensive' diff than I'd intended. That's what
+       I get for working for 1/2 a week at my parents place without real net ;)
+       
+       Cleanups in the makefile and readme to make installation simpler and more
+       sensical.
+       
+       removed outdated rtmux.pl
+       moved old Interface/Email.pm to bin/rt-mailgate
+       started new baseclass for new mail gateways (Interface/Email.pm)
+       
+       first cut implementation of attachment size limits.
+       
+       switched mysql to use 'longblob' rather than just 'blob', so
+       that users can submit > 64k attachments.  with the next release
+       of DBIx::DBSchema, we should have what we need to get
+       PG attachmetns of more reasonable sizes working.
+       
+       first cut implementation of base64 encoding for MIME objects
+       on databases that don't support BLOBs (postgres)
+       
+       restructured initdb so we'll be able to support databases
+       for which we need to hand-hack schema.
+       
+       added the 'Owner' pseudogroup to insertdata
+       
+       fixed the bug in logging for the mail gateway that caused
+       lots of warnings about joins.
+       
+       lots of work on the mail gateway to start to clean up and restructure
+       things.
+       
+2001-03-10 18:53  jesse
+
+       * tools/initdb:
+
+       Refactoring initdb to make it easier to do things like add oracle support. yay
+       
+2001-03-09 15:19  jesse
+
+       * lib/RT/Condition/StatusChange.pm:
+
+       file StatusChange.pm was initially added on branch rt-1-1.
+       
+2001-03-09 15:19  jesse
+
+       * Makefile, etc/config.pm, lib/RT/Attachment.pm, lib/RT/Link.pm,
+       lib/RT/Ticket.pm, lib/RT/Transaction.pm, lib/RT/Watcher.pm,
+       lib/RT/Action/SendEmail.pm, lib/RT/Condition/AnyTransaction.pm,
+       lib/RT/Condition/Generic.pm, lib/RT/Condition/StatusChange.pm,
+       tools/insertdata, tools/testdeps:
+
+       OnResolve scrip now works.
+       some cleanup on Scrips and Watchers.
+       commented out some debug messages that aren't immediately useful
+       
+2001-03-09 15:15  jesse
+
+       * webrt/Ticket/: ModifyDates.html, Elements/EditWatchers:
+
+       Two bugfixes:
+               one, editdates no longer errors out on 5.005
+               two, links from watchers to the ModifyUser page now work properly
+       
+2001-03-08 15:21  jesse
+
+       * Makefile, lib/RT/User.pm, tools/insertdata:
+
+       
+       Fixed a bootstrapping bug introduced with the new acls on Create.
+       bumped version to 1.3.49_01
+       
+2001-03-07 00:40  jesse
+
+       * lib/: test.pl, RT/ACE.pm, RT/CurrentUser.pm, RT/Group.pm,
+       RT/GroupMember.pm, RT/Keyword.pm, RT/KeywordSelect.pm, RT/Link.pm,
+       RT/Queue.pm, RT/Scrip.pm, RT/ScripAction.pm, RT/Template.pm,
+       RT/Ticket.pm, RT/Transaction.pm, RT/User.pm, RT/Watcher.pm:
+
+       acls for Delete methods that needed them.
+       flat lockouts on Delete methods that needed them.
+       
+       test.pl got 'use *' added to it
+       
+2001-03-06 18:07  jesse
+
+       * lib/RT/Attachment.pm:
+
+       Backing out changes that broke ->Content
+       
+2001-03-06 17:42  jesse
+
+       * Makefile, lib/RT/Attachment.pm, lib/RT/Record.pm,
+       lib/RT/Interface/Web.pm, webrt/Elements/SelectSortOrder,
+       webrt/Search/Listing.html, webrt/Search/PickRestriction:
+
+       work on Attachments.
+       cleanup on sorting and limiting.
+       
+2001-03-06 16:26  jesse
+
+       * webrt/Search/PickRestriction:
+
+       adding a header
+       
+2001-03-04 19:46  jesse
+
+       * etc/schema.pm:
+
+       Schema change to support postgres
+       
+2001-03-04 19:44  jesse
+
+       * lib/RT/Record.pm:
+
+       Small change for new DBIx::SearchBuilder::Record PrimaryKey behaviors..
+       
+2001-03-04 19:15  jesse
+
+       * bin/rt:
+
+       rt --id=<int> should work now
+       
+2001-03-02 00:26  jesse
+
+       * etc/config.pm, lib/RT/Date.pm, lib/RT/Ticket.pm,
+       webrt/Ticket/ModifyDates.html:
+
+       first pass on dates and timezones...seems to work. for common ops
+       
+2001-02-28 16:34  jesse
+
+       * lib/RT/User.pm:
+
+       fix for RT/fsck.com: Ticket #208 User->Create needs more argument checking
+               added a check to User->Create to make sure a name was specified
+       
+2001-02-28 08:56  jesse
+
+       * Makefile, bin/rtadmin:
+
+       Enabled the ability to set a user's password via the CLI
+       
+2001-02-27 22:09  jesse
+
+       * lib/Makefile.PL, tools/testdeps:
+
+       Bumping required version of searchbuilder in testdeps and lib/Makefile.pl
+       
+2001-02-27 22:00  jesse
+
+       * webrt/Elements/SelectKeyword:
+
+       allowing undef keywords
+       
+2001-02-27 21:43  jesse
+
+       * webrt/Admin/Queues/Scrips.html:
+
+       Fix for ticket #50. grammar in per queue scrip descriptions
+       
+2001-02-27 21:41  jesse
+
+       * tools/insertdata:
+
+       resolving ticket #181- extra \s in the autoreply template
+       
+2001-02-27 21:36  jesse
+
+       * Makefile, lib/RT/Transaction.pm, tools/testdeps:
+
+       testdeps now erorrs out if you don't specify a db type.
+       
+       queue change transaction descriptions are better.
+       
+       bumped the version
+       
+2001-02-27 21:12  jesse
+
+       * webrt/Elements/SelectTicketSortBy:
+
+       file SelectTicketSortBy was initially added on branch rt-1-1.
+       
+2001-02-27 21:12  jesse
+
+       * webrt/Elements/SelectSortOrder:
+
+       file SelectSortOrder was initially added on branch rt-1-1.
+       
+2001-02-27 21:12  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/Tickets.pm, lib/RT/Interface/Web.pm,
+       webrt/Elements/SelectKeyword, webrt/Elements/SelectResultsPerPage,
+       webrt/Elements/SelectSortOrder, webrt/Elements/SelectTicketSortBy,
+       webrt/Search/Listing.html, webrt/Search/PickRestriction:
+
+       Basic ticket sorting has been implemented.
+       Ticket listing by pages works too.
+       
+       a bit of cleanup in the ticket listing.  now select by keywords uses the component designed for that purpose
+       
+2001-02-26 15:44  jesse
+
+       * lib/RT/Ticket.pm:
+
+       No longer generate spurious watchers transactions when merging tickets.
+       
+2001-02-26 03:08  jesse
+
+       * lib/RT/Ticket.pm:
+
+       fixed a typo in a debug message that prevented compilation
+       
+2001-02-26 02:53  jesse
+
+       * Makefile:
+
+       bumped the version for release. again.
+       
+2001-02-26 02:52  jesse
+
+       * lib/RT/: ACE.pm, Ticket.pm:
+
+       Fixed a variable redef in ticket and an acl granting bug in ACE
+       
+2001-02-26 01:24  jesse
+
+       * Makefile, webrt/Ticket/ModifyLinks.html:
+
+       A bit more merging support.
+       Bumped version for release
+       
+2001-02-25 22:28  jesse
+
+       * webrt/Ticket/Elements/: ShowHistory, ShowTransaction:
+
+       Cleaning up the transaction display.
+       
+2001-02-25 22:22  jesse
+
+       * lib/RT/: Scrip.pm, ScripCondition.pm, Ticket.pm:
+
+       Some bug fixes for merging.
+       Fixed a bug in Scrips that caused an infinite loop if the user had no rights.
+       
+2001-02-24 02:42  jesse
+
+       * Makefile, lib/RT/Ticket.pm:
+
+       Fixed a bug that prevented ticket loading from appearing to work
+       
+2001-02-23 19:17  jesse
+
+       * Makefile, lib/RT/Ticket.pm, lib/RT/Interface/Web.pm:
+
+       Fixed a typo in Ticket.pm that prevented transaction lists from working
+       fixed a paste error in Web.pm
+       
+2001-02-23 17:41  jesse
+
+       * Makefile:
+
+       Bumped the version # for release
+       
+2001-02-23 17:31  jesse
+
+       * Makefile, README, bin/initacls.mysql, bin/webmux.pl,
+       lib/RT/ACE.pm, lib/RT/ACL.pm, lib/RT/Link.pm, lib/RT/Queue.pm,
+       lib/RT/Ticket.pm, lib/RT/User.pm, lib/RT/Interface/Web.pm,
+       tools/initdb, tools/testdeps:
+
+       Fixed a bug that made a first install with mysql fail
+       yanked a UNIVERSAL::AUTOLOADER handler I inserted to test things out.
+       Fixed several ACL creation bugs.
+       Initial implementation of merge for the core.
+       Testdeps now does database testing.
+       
+       Made Ticket->Load smarter. (nolonger needs a seperate LoadByURI sub.
+       
+       fixed bugs in the readme
+       
+2001-02-22 01:03  jesse
+
+       * tools/testdeps:
+
+       Updated dependencies.
+       
+2001-02-21 17:49  jesse
+
+       * lib/RT/: Ticket.pm, Tickets.pm:
+
+       Cache Queue in Ticket.pm for added perf.
+       formatting cleanup in Tickets.pm
+       
+2001-02-20 14:38  jesse
+
+       * webrt/Ticket/Display.html:
+
+       Uncompressed history on summary page...
+       
+2001-02-20 14:18  jesse
+
+       * lib/RT/ScripCondition.pm:
+
+       fixed a pod typo
+       
+2001-02-20 14:07  jesse
+
+       * Makefile, etc/schema.pm, lib/MANIFEST, tools/insertdata,
+       webrt/Admin/Users/Modify.html,
+       webrt/SelfService/Elements/ShowTransaction,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       removed a false unique index from schema. pm.
+       make web interface and insertdata deal properly with the fact that email addresses
+       should be unique.
+       
+       cleaned up ShowTransaction for the webui. (now uses CreatorObj rather than Creator,
+       as it was confusing
+       
+2001-02-20 01:42  jesse
+
+       * lib/RT/: ACE.pm, ACL.pm, Attachment.pm, Attachments.pm,
+       CurrentUser.pm, EasySearch.pm, Group.pm, GroupMember.pm,
+       GroupMembers.pm, Groups.pm, Keyword.pm, KeywordSelect.pm,
+       Keywords.pm, Link.pm, Links.pm, ObjectKeyword.pm,
+       ObjectKeywords.pm, Queue.pm, Queues.pm, Record.pm, Scrip.pm,
+       ScripAction.pm, ScripActions.pm, ScripCondition.pm,
+       ScripConditions.pm, Scrips.pm, Template.pm, Templates.pm,
+       Ticket.pm, Tickets.pm, Transaction.pm, Transactions.pm, User.pm,
+       Users.pm, Utils.pm, Watcher.pm, Watchers.pm, Action/SendEmail.pm:
+
+       A major security audit of the codebase has taken place.  I was primarily out for
+       ACL related issues, but took care of a number of other minor cleanups at the same time.
+       
+2001-02-19 17:16  jesse
+
+       * NEWS:
+
+       fixed a typo in cli_respond_req
+       
+2001-02-15 18:02  jesse
+
+       * webrt/SelfService/: Details.html, Error.html:
+
+       fixing bug #90: SelfService calls to Abort use the 'standard' handler. which includes the wrong header.
+       
+2001-02-15 18:02  jesse
+
+       * webrt/SelfService/Error.html:
+
+       file Error.html was initially added on branch rt-1-1.
+       
+2001-02-15 17:39  jesse
+
+       * webrt/Admin/Elements/SelectSingleOrMultiple:
+
+       file SelectSingleOrMultiple was initially added on branch rt-1-1.
+       
+2001-02-15 17:39  jesse
+
+       * lib/RT/Keyword.pm, lib/RT/KeywordSelect.pm,
+       webrt/Admin/Elements/SelectKeywordSelect,
+       webrt/Admin/Elements/SelectSingleOrMultiple,
+       webrt/Admin/Global/Keywords.html, webrt/Admin/Queues/Keywords.html,
+       webrt/Elements/SelectKeyword, webrt/Elements/SelectKeywordOptions,
+       webrt/Ticket/Elements/ModifyTicket:
+
+       KeywordSelect.pm got an ACL bug fixed.
+       Keyword.pm now does recursive searches in Descendants properly.
+       
+       KeyworSelect editing via the webui is now much smarter. and cleaner. and better. and faster, and resolving bug 123
+       
+2001-02-15 17:39  jesse
+
+       * webrt/Admin/Elements/SelectKeywordSelect:
+
+       file SelectKeywordSelect was initially added on branch rt-1-1.
+       
+2001-02-13 22:54  jesse
+
+       * lib/: Makefile.PL, RT/ACE.pm, RT/Keyword.pm, RT/KeywordSelect.pm,
+       RT/Scrip.pm:
+
+       Better ACL handling for ACEs, Keywords and KeywordSelects.
+       specified more accurate versions of some dependencies in lib/Makefile.PL
+       
+2001-02-13 12:51  jesse
+
+       * bin/rtadmin, etc/schema.pm, lib/RT/Keyword.pm,
+       lib/RT/KeywordSelect.pm, lib/RT/KeywordSelects.pm,
+       lib/RT/Keywords.pm, lib/RT/Queue.pm, lib/RT/Queues.pm,
+       lib/RT/User.pm, lib/RT/Users.pm, webrt/Admin/Global/Keywords.html,
+       webrt/Admin/Keywords/index.html, webrt/Admin/Queues/Keywords.html,
+       webrt/Admin/Queues/Modify.html, webrt/Admin/Users/Modify.html:
+
+       Implemented 'Disabled' for Keywords, KeywordSelects, Queues and Users.
+       This supplants 'Delete' for these objects which need to exist to guarantee
+       referential integrity.
+       
+       Implemented cli and web interfaces to support the new code.
+       
+2001-02-12 18:27  jesse
+
+       * README, bin/rtadmin, etc/schema.pm, lib/RT/Attachment.pm,
+       lib/RT/EasySearch.pm, lib/RT/Link.pm, lib/RT/ObjectKeyword.pm,
+       lib/RT/Queue.pm, lib/RT/Queues.pm, lib/RT/Record.pm,
+       lib/RT/ScripAction.pm, lib/RT/ScripCondition.pm,
+       lib/RT/Template.pm, lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       lib/RT/Watcher.pm, tools/testdeps:
+
+       Cleaned up LastUpdated / Created/ LastUpdatedBy / Creator to all get set on
+       create for all relevant objects.
+       
+       Based on discussions with ivan, changed the 'Deleted' flag on various objects to 'Disabled'
+       to better represent its purpose.
+       
+       Fully implemented Disable for queue as a prototype.
+       
+       fixed queue attribute editing in cli.
+       
+2001-02-12 13:59  jesse
+
+       * tools/testdeps:
+
+       specified a minimum known good version of Getopt::Long
+       
+2001-02-12 12:38  jesse
+
+       * lib/RT/Queue.pm, webrt/Ticket/Elements/ShowTransaction:
+
+       transaction links work again.
+       admin ccs get listed.
+       
+2001-02-11 22:16  jesse
+
+       * Makefile:
+
+       bumped the version # for a new release
+       
+2001-02-11 22:13  jesse
+
+       * lib/RT/Queue.pm, lib/RT/Template.pm, tools/insertdata:
+
+       cleaning up queue create.
+       
+2001-02-11 22:12  jesse
+
+       * etc/schema.pm:
+
+       started the work on dealing with Deleting records that need to exist to guarantee ref. integrity
+       
+2001-02-11 18:40  jesse
+
+       * NEWS:
+
+       imported brandon's url importing code
+       
+2001-02-06 22:59  jesse
+
+       * lib/RT/Queue.pm, webrt/Admin/Queues/Modify.html,
+       webrt/Admin/Users/index.html:
+
+       Fixes to Queue.pm to stop it from clobbering the Name.
+       
+       fixed a typo in the webui Admin/Users/index.html
+       
+2001-02-06 15:22  jesse
+
+       * lib/RT/Queue.pm, webrt/Admin/Elements/QueueRightsForUser,
+       webrt/Admin/Elements/SelectRights,
+       webrt/Ticket/Elements/ShowMembers:
+
+       ACL fixes in Queue.pm.
+       ACL search fies for the web ui.
+       fixed a recursive subticket display bug.
+       
+2001-02-06 12:49  jesse
+
+       * Makefile:
+
+       removed the old rt 1.0 lib/rt directory forever. yay
+       
+2001-02-06 12:48  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       Added a tiny patch from ivan to not extract nested messages. not totally sure about this one yet.
+       
+2001-02-02 01:26  jesse
+
+       * Makefile:
+
+       version bumpin
+       
+2001-02-02 01:11  jesse
+
+       * bin/rtadmin, lib/RT/Keyword.pm:
+
+       keyword creation, editing and deletion now works via the cli tool.
+       
+       This means that for alpha 4, we're *gasp* feature complete.  now to fix lots of bugs.
+       
+2001-02-01 02:46  jesse
+
+       * bin/rtadmin, lib/RT/Group.pm, lib/RT/Keyword.pm,
+       lib/RT/KeywordSelect.pm, tools/testdeps, webrt/Ticket/Display.html,
+       webrt/Ticket/Elements/ShowHistory,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Commandline editing of keywordselects now works.
+       
+       Added in a patch to collapse history on the ticket listing
+       page from Byron Ellacott
+       
+       Bumped a few dependecy versions. added a File::Spec dependency, since File::Temp
+       gets it not quote right.
+       
+2001-01-31 02:08  jesse
+
+       * bin/rtadmin, lib/RT/Group.pm, webrt/Admin/Groups/Members.html:
+
+       Commandline group editing now works. yay. group membership even.
+       the api changed to be easier to work with and more consistent with other functions
+       
+2001-01-30 23:12  jesse
+
+       * lib/RT/Interface/Email.pm, webrt/Ticket/Elements/ShowLinks,
+       webrt/Ticket/Elements/ShowRequestor:
+
+       The email interface now uses tempdir from File::Temp for increased security.
+       
+       A couple of webui cleanups to display more sensible things.
+       
+2001-01-30 16:42  jesse
+
+       * etc/config.pm, lib/RT/User.pm, lib/RT/Interface/Email.pm:
+
+       Added mail-on-error to the mail gateway. now it should tell you when it fails.
+       
+       turned off debugging output from ACLs....
+       
+2001-01-29 23:58  jesse
+
+       * bin/rtadmin, lib/RT/ACE.pm, lib/RT/ACL.pm, lib/RT/Scrip.pm,
+       lib/RT/ScripAction.pm, lib/RT/ScripCondition.pm, lib/RT/Scrips.pm,
+       lib/RT/Template.pm, lib/RT/Templates.pm, lib/RT/User.pm,
+       lib/RT/Interface/CLI.pm, lib/RT/Interface/Web.pm,
+       webrt/Admin/Elements/SelectTemplate,
+       webrt/Admin/Global/Template.html,
+       webrt/Admin/Global/Templates.html,
+       webrt/Admin/Queues/Template.html:
+
+       A bunch of work on the admin cli (acl editor now works)
+       
+       a couple API changes to standardize method names across classes
+       template editing via the web should work better now.
+       
+2001-01-29 23:56  jesse
+
+       * webrt/Elements/: TitleBoxEnd, TitleBoxStart:
+
+       Futzing with the title box to make it a bit more consistent
+       
+2001-01-29 23:54  jesse
+
+       * lib/RT/Record.pm:
+
+       Removed a redundant sub new
+       
+2001-01-29 22:22  jesse
+
+       * lib/RT/Group.pm:
+
+       Groups can now be loaded by name.
+       
+2001-01-23 15:46  jesse
+
+       * bin/rtadmin, etc/schema.pm, lib/RT/Queue.pm, lib/RT/Template.pm,
+       lib/RT/Ticket.pm, lib/RT/User.pm, lib/RT/Interface/CLI.pm,
+       tools/initdb:
+
+       initdb now punts on database creation for oracle.
+       
+       Ticket->Name removed from code (retargeted to post 2.0 and its own table)
+       Some better error checking in User and Queue.
+       
+       Can now edit templates (And create them) from the CLI.
+       
+2001-01-22 12:51  jesse
+
+       * lib/RT/Action/Notify.pm:
+
+       
+       It's amazing how much effect 3 little letters can have. with this patch,
+       scrips that depend on notify (most everything other than autoreply)
+       just work. woo!
+       
+2001-01-22 06:18  jesse
+
+       * lib/RT/Template.pm:
+
+       Fixed a couple ACL bugs in Template::_Set
+       
+2001-01-22 05:04  jesse
+
+       * Makefile:
+
+       Bumped the version # because of the schema change
+       
+2001-01-22 04:51  jesse
+
+       * lib/RT/Interface/CLI.pm:
+
+       file CLI.pm was initially added on branch rt-1-1.
+       
+2001-01-22 04:51  jesse
+
+       * lib/RT/Interface/CLI.pm:
+
+       Added CLI.pm, which rt and adminrt are dependant on
+       
+2001-01-22 04:46  jesse
+
+       * bin/rtadmin, lib/MANIFEST, lib/RT/Template.pm:
+
+       a bit more work on rtadmin (the start of template editing)
+       
+       added Interface/CLI to the manifest.
+       added a comment to Template.pm
+       
+2001-01-22 02:51  jesse
+
+       * Makefile, TODO, bin/mason_handler.scgi, bin/rt, bin/rtadmin,
+       bin/rtmux.pl, bin/webmux.pl, etc/config.pm, etc/schema.Oracle,
+       etc/schema.pm, lib/RT.pm, lib/RT/Attachment.pm,
+       lib/RT/CurrentUser.pm, lib/RT/Group.pm, lib/RT/Link.pm,
+       lib/RT/ObjectKeywords.pm, lib/RT/Queue.pm, lib/RT/Template.pm,
+       lib/RT/Ticket.pm, lib/RT/Tickets.pm, lib/RT/Transaction.pm,
+       lib/RT/User.pm, lib/RT/Users.pm, lib/RT/Watchers.pm,
+       lib/RT/Interface/Email.pm, tools/insertdata,
+       webrt/Admin/Elements/CreateQueueCalled,
+       webrt/Admin/Elements/CreateUserCalled,
+       webrt/Admin/Elements/GrantQueueRightsTo,
+       webrt/Admin/Elements/ModifyKeywordSelect,
+       webrt/Admin/Elements/ModifyQueue,
+       webrt/Admin/Elements/ModifyTemplate,
+       webrt/Admin/Elements/ModifyUser,
+       webrt/Admin/Elements/SelectModifyQueue,
+       webrt/Admin/Elements/SelectModifyUser,
+       webrt/Admin/Elements/SelectTemplate,
+       webrt/Admin/Elements/SelectUsers, webrt/Admin/Global/Scrips.html,
+       webrt/Admin/Global/Template.html,
+       webrt/Admin/Global/Templates.html,
+       webrt/Admin/Global/UserRights.html,
+       webrt/Admin/Groups/Members.html,
+       webrt/Admin/KeywordSelects/index.html, webrt/Admin/Queues/ACL.html,
+       webrt/Admin/Queues/Create.html, webrt/Admin/Queues/Keywords.html,
+       webrt/Admin/Queues/Modify.html, webrt/Admin/Queues/People.html,
+       webrt/Admin/Queues/Scrips.html, webrt/Admin/Queues/Template.html,
+       webrt/Admin/Queues/Templates.html, webrt/Admin/Queues/index.html,
+       webrt/Admin/Users/Modify.html, webrt/Admin/Users/Prefs.html,
+       webrt/Admin/Users/index.html, webrt/Elements/Header,
+       webrt/Elements/MyRequests, webrt/Elements/MyTickets,
+       webrt/Elements/Quicksearch, webrt/Elements/SelectNewTicketQueue,
+       webrt/Elements/SelectOwner, webrt/Elements/SelectQueue,
+       webrt/Elements/SelectUsers, webrt/SelfService/Details.html,
+       webrt/SelfService/Elements/Header,
+       webrt/SelfService/Elements/MyRequests,
+       webrt/SelfService/Elements/ShowTransaction,
+       webrt/Ticket/Create.html, webrt/Ticket/ModifyLinks.html,
+       webrt/Ticket/autohandler, webrt/Ticket/Elements/AddWatchers,
+       webrt/Ticket/Elements/ShowBasics, webrt/Ticket/Elements/ShowDates,
+       webrt/Ticket/Elements/ShowDependencies,
+       webrt/Ticket/Elements/ShowLinks, webrt/Ticket/Elements/ShowPeople,
+       webrt/Ticket/Elements/ShowReferences,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       In prep for a true schema freeze, I fixed a number of attribute names that were troublesome for
+       one reason or another.
+       
+               Queue->QueueId and User->UserId have been changed to Queue->Name and User->Name, so
+               as not to conflict with queue->id and user->id any more.  A number of tables
+               used "Alias" and "Title" and things like that, rather than the somewhat
+               more standard "Name" and "Description"
+       
+       Abstracted a bunch of the CLI setup stuff out into lib/RT/Interface/CLI
+       
+       Abstracted some of the core DB startup code into RT.pm
+       
+       fixed the makefile to handle the new rtadmin.
+       
+       lots of updates to the new rtadmin (which works for user editing!)
+       
+       fixed a bug in ObjectKeywords->LoadByName....
+       
+2001-01-19 04:23  jesse
+
+       * bin/rtadmin:
+
+       A bit of cleanup and a bit more docs.  it's now actually implementable with getopt::long
+       
+2001-01-19 04:16  jesse
+
+       * bin/rtadmin:
+
+       file rtadmin was initially added on branch rt-1-1.
+       
+2001-01-19 04:16  jesse
+
+       * bin/rtadmin:
+
+       Docced the proposed cli for the new rtadmin tool. It's rather more intense than I'd intended
+       
+2001-01-19 02:15  jesse
+
+       * lib/RT/User.pm:
+
+       Fixed ACL bug that prevented by-group auth from working
+       
+2001-01-19 00:52  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/User.pm, webrt/SelfService/Details.html:
+
+       Work on SelfService (fixing the a permission denied bug)
+       
+2001-01-19 00:21  jesse
+
+       * webrt/NoAuth/webrt.css:
+
+       file webrt.css was initially added on branch rt-1-1.
+       
+2001-01-19 00:21  jesse
+
+       * webrt/: webrt.css, Elements/Header, NoAuth/webrt.css,
+       SelfService/Details.html, SelfService/Elements/Header:
+
+       webui cleanups
+       
+2001-01-19 00:07  jesse
+
+       * bin/rt:
+
+       bug-fixing in bin/rt
+       
+2001-01-18 23:47  jesse
+
+       * lib/RT/Link.pm, lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       webrt/Ticket/ModifyLinks.html, webrt/Ticket/Elements/ShowLinks,
+       webrt/Ticket/Elements/ShowMembers,
+       webrt/Ticket/Elements/ShowReferences,
+       webrt/Ticket/Elements/ShowSummary:
+
+       A bunch of work on links.
+       API cleanups.
+       Ticket-> Members and MemberOf  now return Links objects rather than Tickets Objects
+       Edit links via the webui works now
+       
+2001-01-18 15:11  jesse
+
+       * HACKING, docs/API:
+
+       Added a bit more documentation and philosophy
+       to the API file. yanked tobix' old out of date "HACKING"
+       doc
+       
+2001-01-18 03:18  jesse
+
+       * Makefile, NEWS:
+
+       Headers in incoming mail are now parsed case insensitively.
+       
+       Version is now 1.0.7
+       
+2001-01-17 16:41  jesse
+
+       * Makefile, tools/testdeps:
+
+       updated searchbuilder dependency in testdeps
+       
+       revved version in Makefile for release.
+       
+2001-01-17 14:27  jesse
+
+       * Makefile, bin/rt, lib/RT/Date.pm:
+
+       Fixed #45 Permissions bug on redhat 6.0
+       Fixed #51 RT CLI doesn't check for valid user
+       
+       Implemented a bunch more searching in bin/rt
+       
+       Refactored RT::Date a bit.
+       
+       Fixed some unreported permissions eits.
+       
+2001-01-16 11:43  jesse
+
+       * webrt/Ticket/Elements/: ShowLinks, ShowMemberOf, ShowMembers,
+       ShowPeople, ShowSummary:
+
+       Cleaned up and shortened the Ticket Display screen
+       
+2001-01-16 10:56  jesse
+
+       * lib/RT/Ticket.pm, webrt/Ticket/Modify.html:
+
+       Cleaned up and better documented Ticket->SetStatus
+       Ticket/Modify.html got rid of a whole bunch of evals.
+       
+2001-01-15 23:57  jesse
+
+       * Makefile, lib/MANIFEST, lib/RT/Tickets.pm:
+
+       Removed a deleted file from the manifest. revved the version
+       
+2001-01-15 15:19  jesse
+
+       * lib/RT/: KeywordSelect.pm, Queue.pm:
+
+       Adding the ability to load keyword selects by name.
+       
+2001-01-15 03:49  jesse
+
+       * lib/RT/Ticket.pm, webrt/Ticket/Elements/EditKeywordSelects:
+
+       keyword display now cleaner. doesn't force you to select a keyword if none
+       is selected yet.
+       
+2001-01-15 03:36  jesse
+
+       * lib/RT/Ticket.pm:
+
+       SetQueue is no-longer heinously broken. thanks eoin
+       
+2001-01-15 03:10  jesse
+
+       * lib/RT/: Queue.pm, User.pm, Action/Notify.pm, Action/Spam.pm:
+
+       removed an old RT::Action that didn't actually do anything useful these days.  it'll get replaced later.
+       
+       Scrips which sign watchers up for things _actually seem to work_
+       (required some frobbing of watchers)
+       
+2001-01-15 02:44  jesse
+
+       * bin/rt, etc/schema.pm, lib/RT/ObjectKeywords.pm, lib/RT/Queue.pm,
+       lib/RT/Template.pm, lib/RT/Ticket.pm, lib/RT/Tickets.pm,
+       lib/RT/Watchers.pm, webrt/Elements/ViewUser,
+       webrt/Ticket/Elements/ShowRequestor:
+
+       Added a 'Type' column to templates, to more easily allow for insertable templates for quick responses from customer service folks ,etc.
+       Reworked a bunch of the watchers code to not do its own potentially very bogus caching and to reuse more core code.
+       Oh. and it always returns the expected object type now.
+       
+       Redid tobias' old "ViewUser" box to use a set of tickets, rather than a bogus search for watchers (ignoring tickets).
+       
+       Fixed a bug in searching for tickets by watcher.
+       
+2001-01-15 00:53  jesse
+
+       * bin/rt, webrt/Elements/SelectOwner, webrt/Elements/TitleBoxStart,
+       webrt/NoAuth/Login.html, webrt/Ticket/Elements/ShowSummary:
+
+       Added a bunch of doc to bin/rt. probably should have check to see if it runs ;)
+       TitleBoxStart got the attributes:  class, title_href and titleright_href. makes calls to it cleaner.
+       showsummary got a few more links to the places things should go.
+       selectOwner now will actually default to nobody if that's the right value.
+       
+2001-01-13 03:22  jesse
+
+       * TODO:
+
+       Changed the TODO file to point to the current TODO list
+       
+2001-01-13 03:18  jesse
+
+       * Makefile:
+
+       Bumped version # for release
+       
+2001-01-13 03:11  jesse
+
+       * bin/rtmux.pl, lib/RT/Queue.pm, lib/RT/Action/SendEmail.pm:
+
+       Fixing a couple minor things in rtmux and Queue.pm so that the mail interfaces work ok.
+       
+2001-01-13 02:34  jesse
+
+       * lib/RT/User.pm:
+
+       Added the ability to generate a random password to RT::User.  we don't yet
+       have a nice way to email this information to the user. which is a real bummer.
+       
+2001-01-12 17:35  jesse
+
+       * etc/config.pm, lib/RT/Ticket.pm, lib/RT/User.pm,
+       lib/RT/Interface/Web.pm, webrt/Admin/Users/Modify.html,
+       webrt/SelfService/Create.html, webrt/SelfService/Details.html,
+       webrt/SelfService/Elements/Header, webrt/SelfService/Elements/Tabs:
+
+       Made ACLs for 'Everyone' work. fixed a few other small ACL bugs.
+       Made the 'Requestor-mode' ticket create tool work.
+       
+2001-01-12 12:27  jesse
+
+       * webrt/Elements/: SelectDate, SelectOwner:
+
+       Commented out unimplemented select relative date bits.
+       SelectOwner now lets you pick nobody.
+       
+2001-01-12 03:16  jesse
+
+       * bin/rt, bin/rtmux.pl, lib/RT/Ticket.pm, tools/testdeps:
+
+       Comments and Correspondence work from the cli now. with and without --source (for source data) and --no-edit (to disable invocation of $EDITOR)
+       
+       rt and rtmux.pl now drop setgidness as soon as they can. it's happier this
+       way. really :)
+       
+       testdeps now installs File::Temp, which is needed to do safe tempfile usage.
+       
+2001-01-12 00:11  jesse
+
+       * webrt/Elements/: MyRequests, MyTickets:
+
+       Bug fix for #47 (Id flows into subject on 'home' screen)
+       
+2001-01-11 03:34  jesse
+
+       * webrt/Elements/Header:
+
+       Removed extra spaces from the header
+       
+2001-01-11 02:57  jesse
+
+       * webrt/Elements/SelectOwner:
+
+       Made SelectOwner actually show all the right people
+       
+2001-01-11 02:53  jesse
+
+       * tools/testdeps:
+
+       Added a requirement for the current DBIx::SearchBuilder to testdeps
+       
+2001-01-11 02:28  jesse
+
+       * Makefile:
+
+       revved the version for distribution
+       
+2001-01-11 02:28  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/Interface/Web.pm,
+       webrt/Elements/Quicksearch, webrt/Elements/TitleBoxStart,
+       webrt/Ticket/Display.html, webrt/Ticket/Elements/EditBasics:
+
+       fixed a couple of UI bugs. the 'quick' links in the web ticket view should now all work.
+       Logger->warn  -> Logger->warning in the web ui
+       
+2001-01-10 17:42  jesse
+
+       * Makefile:
+
+       Bumped to 1.3.33
+       
+2001-01-10 01:46  jesse
+
+       * webrt/Ticket/Attachment/dhandler:
+
+       Date: Wed, 10 Jan 2001 16:22:43 +1000 (EST)                                     From: Byron Ellacott <bje@apnic.net>                                            To: rt-devel@fsck.com                                                           Subject: Re: [rt-devel] [rt-announce] RT 1.3.30 released                        List-Id: RT Development <rt-devel.lists.fsck.com>                                                                                                               [-- Attachment #1 --]                                                           [-- Type: text/PLAIN, Encoding: 7bit, Size: 0.7K --]                                                                                                            On Wed, 3 Jan 2001, Jesse wrote:                                                                                                                                [snip]                                                                                                                                                          I noticed 1-3-31 sitting around, so I installed that.  I'm giving a             demonstration tomorrow, so I've been running through the web UI.  I             noticed a problem with attachments - HTML::Mason was munging everything to      be HTML-nice, but a jpg doesn't display if its < characters are changed to      &lt;, and it's not good to have </BODY></HTML> on the end of binary             attachments.
+       
+2001-01-10 00:24  jesse
+
+       * Makefile:
+
+       Revved the version to 1.3.32
+       
+2001-01-09 23:57  jesse
+
+       * webrt/Admin/Users/Rights.html:
+
+       file Rights.html was initially added on branch rt-1-1.
+       
+2001-01-09 23:57  jesse
+
+       * lib/RT/Record.pm, lib/RT/User.pm, lib/RT/Interface/Email.pm,
+       tools/insertdata, webrt/Admin/Elements/UserTabs,
+       webrt/Admin/Users/Modify.html, webrt/Admin/Users/Rights.html,
+       webrt/Elements/Quicksearch, webrt/Elements/TitleBoxEnd,
+       webrt/Elements/TitleBoxStart:
+
+       Did some work on User creation to check for duplicate userids on create.
+       Better user create error checking.
+       Switched the TitleBox UI to a compromise between new and old.
+       
+       Did a bit of ui work on web based user modification
+       
+2001-01-09 02:15  jesse
+
+       * Makefile, lib/RT/User.pm, tools/insertdata,
+       webrt/Admin/Users/Modify.html:
+
+       shuffling around hte user password stuff. hopefully the user modification issues seen in 1.3.30 will be fixed.
+       
+2001-01-09 00:48  jesse
+
+       * webrt/Ticket/History.html:
+
+       file History.html was initially added on branch rt-1-1.
+       
+2001-01-09 00:48  jesse
+
+       * docs/keywords:
+
+       file keywords was initially added on branch rt-1-1.
+       
+2001-01-09 00:48  jesse
+
+       * bin/mason_handler.scgi:
+
+       file mason_handler.scgi was initially added on branch rt-1-1.
+       
+2001-01-09 00:48  jesse
+
+       * Makefile, bin/mason_handler.scgi, bin/webmux.pl, docs/keywords,
+       lib/RT/Tickets.pm, lib/RT/Interface/Web.pm,
+       webrt/Admin/Elements/GrantQueueRightsTo~,
+       webrt/Ticket/History.html:
+
+       Started to write out notes on a testsuite.
+       Started work on a CGI/SpeedyCGI frontend
+       added a file that was missing in 1.3.30
+       
+2001-01-08 14:04  jesse
+
+       * NEWS, README:
+
+       Added a web based due-date patch.
+       
+2001-01-06 15:17  jesse
+
+       * etc/schema.pm:
+
+       Added 'Deleted' attributes for tables whose entries can not be safely
+       removed from the database for ref. integrity problems.
+       
+2001-01-03 23:07  jesse
+
+       * Makefile:
+
+       Bumped version to 1.3.30
+       
+2001-01-03 23:02  jesse
+
+       * tools/testdeps:
+
+       file testdeps was initially added on branch rt-1-1.
+       
+2001-01-03 23:02  jesse
+
+       * README, bin/testdeps.pl, lib/Makefile.PL, tools/testdeps,
+       webrt/Elements/TitleBoxStart:
+
+       a bit of quick UI work.
+       an update to depend on DBIx::SearchBuilder 0.15 (just releaseD)
+       testdeps moved to tools
+       
+2001-01-03 03:21  jesse
+
+       * bin/rt, lib/RT/Tickets.pm:
+
+       ticket searches for integers are a bit more flexible now (and the code's tighter)
+       
+       the rtq section of the new bin/rt appears functional, if underdocced.
+       
+2001-01-02 22:55  jesse
+
+       * lib/RT/Tickets.pm, lib/RT/Interface/Web.pm,
+       webrt/Search/PickRestriction:
+
+       Imported ivan's keyword search patch, along with a modification to allow
+       for _not_ selecting keywords.
+       
+2001-01-02 21:45  jesse
+
+       * Makefile, bin/testdeps.pl, lib/RT/KeywordSelect.pm,
+       lib/RT/ObjectKeyword.pm, lib/RT/ObjectKeywords.pm,
+       lib/RT/Ticket.pm, lib/RT/Tickets.pm, lib/RT/Watchers.pm,
+       lib/RT/Interface/Web.pm, tools/insertdata, webrt/webrt.css,
+       webrt/Elements/Header, webrt/Elements/TitleBoxStart,
+       webrt/Elements/ViewUser, webrt/Search/Listing.html,
+       webrt/Search/PickRestriction, webrt/Ticket/Create.html,
+       webrt/Ticket/Display.html, webrt/Ticket/Elements/ShowBasics,
+       webrt/Ticket/Elements/ShowSummary,
+       webrt/Ticket/Elements/ShowTransaction, webrt/Ticket/Elements/Tabs:
+
+       Fixed a warning in testdeps (Well, worked around it, anyway)
+       Added some helper functions for keywords and keyword selects.
+       
+       did a bunch of UI cleanup. started to color-code the webui
+       
+       started work on a generic queue listing format in Tickets.pm
+       
+2001-01-02 15:02  jesse
+
+       * webrt/SelfService/Elements/ShowTransaction:
+
+       file ShowTransaction was initially added on branch rt-1-1.
+       
+2001-01-02 15:02  jesse
+
+       * webrt/SelfService/: Create.html, Details.html,
+       Elements/GotoTicket, Elements/Header, Elements/ShowTransaction,
+       Elements/Tabs:
+
+       Fleshed out more of the requestor UI.
+       It's good enough for the alpha. yay!
+       
+2001-01-02 15:02  jesse
+
+       * webrt/SelfService/Elements/GotoTicket:
+
+       file GotoTicket was initially added on branch rt-1-1.
+       
+2001-01-01 19:06  jesse
+
+       * webrt/Ticket/: Modify.html, ModifyDates.html, ModifyPeople.html,
+       Elements/EditBasics, Elements/EditKeywordSelects,
+       Elements/ShowDates, Elements/ShowSummary:
+
+       Even yet still more Technicolor!
+       
+2001-01-01 18:42  jesse
+
+       * webrt/NoAuth/images/spacer.gif:
+
+       file spacer.gif was initially added on branch rt-1-1.
+       
+2001-01-01 18:42  jesse
+
+       * webrt/NoAuth/Reminder.html:
+
+       file Reminder.html was initially added on branch rt-1-1.
+       
+2001-01-01 18:42  jesse
+
+       * webrt/Elements/Section:
+
+       file Section was initially added on branch rt-1-1.
+       
+2001-01-01 18:42  jesse
+
+       * lib/RT/CurrentUser.pm, lib/RT/KeywordSelect.pm, webrt/webrt.css,
+       webrt/Admin/Keywords/index.html, webrt/Admin/Queues/People.html,
+       webrt/Elements/Header, webrt/Elements/ListActions,
+       webrt/Elements/MyRequests, webrt/Elements/MyTickets,
+       webrt/Elements/Section, webrt/Elements/Submit,
+       webrt/Elements/TitleBoxEnd, webrt/Elements/TitleBoxStart,
+       webrt/NoAuth/Login.html, webrt/NoAuth/Reminder.html,
+       webrt/NoAuth/images/spacer.gif, webrt/Ticket/Display.html,
+       webrt/Ticket/ModifyPeople.html, webrt/Ticket/Elements/ShowSummary,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       A bunch of UI work.
+       Ticket Display is significantly different. and _much_ faster loading.
+       A color style guide for the web ui is forthcoming.
+       
+       removed a Die from CurrentUser.
+       
+2000-12-30 03:27  jesse
+
+       * webrt/Elements/Tabs:
+
+       fixed a ui nit from ivan
+       
+2000-12-30 03:03  jesse
+
+       * webrt/Ticket/Elements/ShowSummary:
+
+       fixed a hyperlink.
+       
+2000-12-30 00:07  jesse
+
+       * Makefile:
+
+       bumped version for release
+       
+2000-12-30 00:03  jesse
+
+       * etc/config.pm, lib/RT/User.pm,
+       webrt/Admin/Elements/ModifyKeywordSelect,
+       webrt/Admin/Global/Keywords.html,
+       webrt/Admin/KeywordSelects/index.html,
+       webrt/Admin/Keywords/Modify.html, webrt/Admin/Keywords/index.html,
+       webrt/Admin/Queues/Keywords.html, webrt/Admin/Users/index.html:
+
+       Fixed a bug in keyword creation that caused urls and param values to get buggered.
+       
+       Passwords are now encrypted in the database
+       
+2000-12-29 05:54  jesse
+
+       * bin/rt, lib/RT/Link.pm, lib/RT/Ticket.pm,
+       lib/RT/Interface/Web.pm, webrt/Ticket/Display.html,
+       webrt/Ticket/Elements/ShowDependencies,
+       webrt/Ticket/Elements/ShowLinks:
+
+       Did a bunch of work on the linking interface. It's now got
+       a much easier API. and it's twice as functional (you can delete links)
+       
+       Did the proof-of concept for the link code in the CLI. it'll need
+       fleshing out tomorrow.
+       
+       Fleshed out other parts of the CLI.
+       
+       stayed up way too late.
+       
+2000-12-29 00:30  jesse
+
+       * Makefile, bin/rt, lib/RT/Keyword.pm, lib/RT/Ticket.pm:
+
+       Added keyword support to the CLI.
+       fixed a couple of Keyword core bugs
+       
+2000-12-28 03:56  jesse
+
+       * bin/rt, bin/testdeps.pl, lib/RT/Keyword.pm,
+       lib/RT/KeywordSelect.pm, lib/RT/User.pm:
+
+       Removed unused keyword related code. added in a bit of
+       new keyword related code to get "relative" paths of keywords.
+       
+       fixed a longstanding warning in ACL checking.
+       
+       first 1/2 of keywords support for the new rt cli.
+       
+       added Tie::IxHash to testdeps.pl (keywords needs it)
+       
+2000-12-27 02:19  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       missed a comma
+       
+2000-12-27 01:21  jesse
+
+       * webrt/Admin/Global/Keywords.html:
+
+       file Keywords.html was initially added on branch rt-1-1.
+       
+2000-12-27 01:21  jesse
+
+       * etc/schema.pm, lib/RT/ACE.pm, lib/RT/KeywordSelect.pm,
+       lib/RT/KeywordSelects.pm, lib/RT/Queue.pm, lib/RT/Ticket.pm,
+       lib/RT/Transaction.pm, webrt/Admin/Elements/SystemTabs,
+       webrt/Admin/Global/Keywords.html, webrt/Admin/Queues/Keywords.html,
+       webrt/Ticket/Create.html, webrt/Ticket/ModifyPeople.html,
+       webrt/Ticket/Elements/EditKeywordSelects,
+       webrt/Ticket/Elements/ShowKeywordSelects,
+       webrt/Ticket/Elements/ShowSummary,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Added the ability to name keyword selects.
+       first round of ACLing for keywords.
+       
+       Added the ability to create KeywordSelects which apply to all queues (Rather than just
+       a single queue)
+       
+       misc bigfixes.
+       
+2000-12-26 02:56  jesse
+
+       * webrt/Elements/SelectKeywordOptions:
+
+       file SelectKeywordOptions was initially added on branch rt-1-1.
+       
+2000-12-26 02:56  jesse
+
+       * webrt/Admin/Keywords/index.html:
+
+       file index.html was initially added on branch rt-1-1.
+       
+2000-12-26 02:56  jesse
+
+       * webrt/Admin/Queues/Keywords.html:
+
+       file Keywords.html was initially added on branch rt-1-1.
+       
+2000-12-26 02:56  jesse
+
+       * webrt/Elements/SelectKeyword:
+
+       file SelectKeyword was initially added on branch rt-1-1.
+       
+2000-12-26 02:56  jesse
+
+       * bin/webmux.pl, etc/schema.pm, lib/RT/Keyword.pm,
+       lib/RT/KeywordSelect.pm, lib/RT/KeywordSelects.pm,
+       lib/RT/Keywords.pm, lib/RT/ObjectKeyword.pm,
+       lib/RT/ObjectKeywords.pm, lib/RT/Queue.pm, lib/RT/Ticket.pm,
+       lib/RT/Watchers.pm, webrt/Admin/Elements/ModifyKeyword,
+       webrt/Admin/Elements/ModifyKeywordSelect,
+       webrt/Admin/Elements/QueueTabs, webrt/Admin/Elements/Tabs,
+       webrt/Admin/KeywordSelects/index.html,
+       webrt/Admin/Keywords/Modify.html, webrt/Admin/Keywords/index.html,
+       webrt/Admin/Queues/Keywords.html, webrt/Elements/SelectKeyword,
+       webrt/Elements/SelectKeywordOptions, webrt/Ticket/Modify.html,
+       webrt/Ticket/Elements/EditKeywordSelects,
+       webrt/Ticket/Elements/ShowKeywordSelects:
+
+       My first pass at making the keywords stuff work more like the rest of the RT core.
+       A couple of minor fixes on non-keywords things, but mostly this was an
+       edit of the keywords code to:
+               1. make the admin UI work more like the rest of the admin ui
+               (still need to do "global" keyword selections)
+       
+               2. bring the coding style of the keywords stuff more into line
+               with the rest of the codebase
+       
+               3. flesh out the helper methods in the keywords modules.
+       
+2000-12-24 02:04  jesse
+
+       * lib/RT/: Keyword.pm, KeywordSelect.pm, ObjectKeyword.pm:
+
+       A first round of commentary and cleanup on a few modules
+       
+2000-12-24 01:34  jesse
+
+       * webrt/Ticket/Elements/ShowKeywordSelects:
+
+       file ShowKeywordSelects was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * webrt/Ticket/Elements/EditKeywordSelects:
+
+       file EditKeywordSelects was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * webrt/Admin/: KeywordSelects/Modify.html, Keywords/Modify.html:
+
+       file Modify.html was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * webrt/Admin/KeywordSelects/index.html:
+
+       file index.html was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * webrt/Admin/Elements/ModifyKeyword:
+
+       file ModifyKeyword was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * webrt/Admin/Elements/ModifyKeywordSelect:
+
+       file ModifyKeywordSelect was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * webrt/Admin/Elements/SelectModifyKeyword:
+
+       file SelectModifyKeyword was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * webrt/Admin/Elements/SelectModifyKeywordSelect:
+
+       file SelectModifyKeywordSelect was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * lib/RT/ObjectKeyword.pm:
+
+       file ObjectKeyword.pm was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * lib/RT/Keyword.pm:
+
+       file Keyword.pm was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * lib/RT/KeywordSelect.pm:
+
+       file KeywordSelect.pm was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * lib/test.pl:
+
+       file test.pl was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * lib/RT/ObjectKeywords.pm:
+
+       file ObjectKeywords.pm was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * lib/RT/KeywordSelects.pm:
+
+       file KeywordSelects.pm was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * lib/RT/Keywords.pm:
+
+       file Keywords.pm was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * Makefile, etc/acl.Pg, etc/schema.pm, lib/MANIFEST,
+       lib/MANIFEST.SKIP, lib/Makefile.PL, lib/RT.pm, lib/test.pl,
+       lib/RT/Keyword.pm, lib/RT/KeywordSelect.pm,
+       lib/RT/KeywordSelects.pm, lib/RT/Keywords.pm,
+       lib/RT/ObjectKeyword.pm, lib/RT/ObjectKeywords.pm,
+       lib/RT/Ticket.pm, webrt/Admin/Elements/ModifyKeyword,
+       webrt/Admin/Elements/ModifyKeywordSelect,
+       webrt/Admin/Elements/SelectModifyKeyword,
+       webrt/Admin/Elements/SelectModifyKeywordSelect,
+       webrt/Admin/Elements/Tabs, webrt/Admin/KeywordSelects/Modify.html,
+       webrt/Admin/KeywordSelects/index.html,
+       webrt/Admin/Keywords/Modify.html, webrt/Ticket/Modify.html,
+       webrt/Ticket/Elements/EditBasics,
+       webrt/Ticket/Elements/EditKeywordSelects,
+       webrt/Ticket/Elements/ShowKeywordSelects,
+       webrt/Ticket/Elements/ShowSummary:
+
+       Importing two largish patches from ivan@420.am:
+               First up, ivan switched lib/RT over to using MakeMaker..so now
+               we get man pages for the core modules and a bunch of other cool stuff.
+       
+               Second: ivan handed us an almost complete keywords system.  Over the next
+               week or two, I'm going to be adding ACL support to the keywords system
+               and starting to integrate bits of it better into the rest of the core
+               (though it's nearly all done already).
+       
+                       Thanks, ivan!
+       
+2000-12-24 01:34  jesse
+
+       * lib/MANIFEST:
+
+       file MANIFEST was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * lib/Makefile.PL:
+
+       file Makefile.PL was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * lib/RT.pm:
+
+       file RT.pm was initially added on branch rt-1-1.
+       
+2000-12-24 01:34  jesse
+
+       * lib/MANIFEST.SKIP:
+
+       file MANIFEST.SKIP was initially added on branch rt-1-1.
+       
+2000-12-23 23:45  jesse
+
+       * Makefile, webrt/Ticket/Elements/Tabs:
+
+       made the links in the ticket tool bar point to the right places.
+       
+2000-12-23 23:15  jesse
+
+       * lib/RT/Ticket.pm:
+
+       fixing a typo and the docs.
+       
+2000-12-23 23:12  jesse
+
+       * lib/RT/Ticket.pm:
+
+       the internal function _SetTold shouldn't be hitting an ACL check.
+       
+2000-12-23 17:37  jesse
+
+       * lib/RT/Ticket.pm, webrt/Ticket/Display.html,
+       webrt/Ticket/Modify.html, webrt/Ticket/ModifyDates.html,
+       webrt/Ticket/ModifyLinks.html, webrt/Ticket/ModifyPeople.html,
+       webrt/Ticket/Update.html, webrt/Ticket/Elements/Tabs,
+       webrt/Ticket/Elements/TicketToolBox:
+
+       Cleaned up a bit of date diff related functionality.
+       
+       Did work on the ticket tool bar.
+       
+2000-12-23 16:46  jesse
+
+       * lib/RT/Record.pm:
+
+       Lets try that again..
+       
+2000-12-23 16:39  jesse
+
+       * lib/RT/Record.pm:
+
+       LongSinceUpdatedAsString is now actually relative :)
+       
+2000-12-23 16:09  jesse
+
+       * lib/RT/Tickets.pm:
+
+       added a default to LimitStatus.
+       
+2000-12-23 01:47  jesse
+
+       * bin/: rt, testdeps.pl:
+
+       updated testdeps to require the latest mason (for ease of debugging)
+       the new combined bin/rt actually implements the behavior of rtq and rt -show
+       fully (I believe). next up, more ticket modification and a code cleanup
+       
+2000-12-22 18:37  jesse
+
+       * Makefile:
+
+       bumped to 1.3.28 for release
+       
+2000-12-19 22:38  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/Tickets.pm, lib/RT/Transaction.pm,
+       lib/RT/Interface/Web.pm, webrt/Elements/Tabs,
+       webrt/Ticket/ModifyPeople.html, webrt/Ticket/Elements/EditPeople,
+       webrt/User/Prefs.html:
+
+       Removed outdated CC request to and BCC request to
+       did some UI touchups.
+       
+       started a bit of work in tickets.pm for the new CLI (more explicit searching)
+       
+2000-12-19 21:50  jesse
+
+       * bin/rt:
+
+       Modifying ticket requestors from the commandline works. yay.
+       Simplified the commandline arguments a bit too.
+       
+2000-12-19 18:37  jesse
+
+       * bin/: mason_handler.fcgi, webmux.pl:
+
+       updaes to webmux.pl to work on with new versions of HTML::Mason
+       
+2000-12-19 14:05  jesse
+
+       * webrt/Ticket/Display.html:
+
+       Fixed a bug in Ticket/Display.html that caused navigation not to work within new tickets.
+       
+2000-12-19 04:03  jesse
+
+       * bin/rt:
+
+       implemented --history (show ticket history)
+       and --limit-status (limit to a ticket status)
+       just to make sure that this works at all.  it does.
+       
+       With the new ui, you'll be able to trivially perform actions on large #s of tickets.
+       
+2000-12-19 03:38  jesse
+
+       * bin/rt:
+
+       After playing with a few Getopt variants, I've settled on Getopt::Long for now...
+       and implemented the skeleton of the new RT cli.  I suspect that with another day's work,
+       it should be mostly functional. yay.
+       
+2000-12-19 03:38  jesse
+
+       * bin/rt:
+
+       file rt was initially added on branch rt-1-1.
+       
+2000-12-18 19:34  jesse
+
+       * docs/design_docs/cli_spec:
+
+       file cli_spec was initially added on branch rt-1-1.
+       
+2000-12-18 19:34  jesse
+
+       * docs/design_docs/cli_spec:
+
+       Added the CLI spec from deborah.
+       
+2000-12-18 02:11  jesse
+
+       * webrt/Elements/: CreateTicket, GotoTicket:
+
+       Fixed a basepath error.
+       
+2000-12-18 01:19  jesse
+
+       * webrt/Elements/CreateTicket:
+
+       file CreateTicket was initially added on branch rt-1-1.
+       
+2000-12-18 01:19  jesse
+
+       * webrt/Elements/GotoTicket:
+
+       file GotoTicket was initially added on branch rt-1-1.
+       
+2000-12-18 01:19  jesse
+
+       * README, webrt/Admin/Queues/Modify.html,
+       webrt/Admin/Users/Modify.html, webrt/Elements/CreateTicket,
+       webrt/Elements/GotoTicket, webrt/Search/Listing.html:
+
+       A few more changes from Lee Ann and a couple of files I missed on the earlier  checkins.
+       
+2000-12-18 01:14  jesse
+
+       * webrt/Admin/Groups/Rights.html:
+
+       file Rights.html was initially added on branch rt-1-1.
+       
+2000-12-18 01:14  jesse
+
+       * webrt/Admin/Groups/: Members.html, Rights.html:
+
+       a couple UI tweaks from Lee Ann Goldstein
+       
+2000-12-18 00:51  jesse
+
+       * webrt/: Admin/Elements/SystemTabs, Admin/Groups/index.html,
+       Admin/Users/index.html, Elements/Submit, Elements/Tabs:
+
+       More UI hacking. whee
+       
+2000-12-17 23:55  jesse
+
+       * webrt/: Elements/SelectNewTicketQueue, Elements/SelectOwner,
+       Elements/Tabs, Ticket/Create.html, Ticket/Elements/Tabs:
+
+       ui hacking to add the "Create Ticket" and "Goto Ticket" ui elements at the top.
+       
+2000-12-17 22:17  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       webrt/Elements/MessageBox, webrt/Ticket/Modify.html,
+       webrt/Ticket/ModifyPeople.html:
+
+       Fixing bugs 7,8,28
+       
+2000-12-17 02:38  jesse
+
+       * webrt/Ticket/Elements/ShowDates:
+
+       *sigh* typo in ShowDates.
+       
+2000-12-17 02:34  jesse
+
+       * lib/RT/Date.pm, webrt/Ticket/Elements/EditDates,
+       webrt/Ticket/Elements/ShowDates:
+
+       A bit of work on Dates. a postgres fix from ivan and some display prettification.
+       
+2000-12-17 02:15  jesse
+
+       * Makefile, webrt/Ticket/Elements/TicketToolBox:
+
+       commeted out some toolbar actions which don't work yet.
+       
+       bumped the version to 1.3.27 for imminent release.
+       
+2000-12-17 01:51  jesse
+
+       * lib/RT/Attachment.pm:
+
+       The ACL problem on attachments was a bootstrapping issue. couldn't check acls because it
+       couldn't necessarily see what to check
+       
+2000-12-17 01:44  jesse
+
+       * lib/RT/Attachment.pm:
+
+       acl fix for attachments.
+       
+2000-12-17 01:15  jesse
+
+       * lib/RT/Ticket.pm:
+
+       fixing ugly disgusting ACL bugs
+       
+2000-12-17 00:35  jesse
+
+       * lib/RT/Transaction.pm, lib/RT/User.pm,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Fixing bugs in Transaction ACLs.
+       
+2000-12-16 03:56  jesse
+
+       * webrt/Admin/Groups/Members.html:
+
+       ui nit in admin/groups/members.
+       
+2000-12-16 03:31  jesse
+
+       * lib/RT/: Group.pm, GroupMember.pm:
+
+       $object->$SUPER::Foo is wrong. $object->SUPER::Foo is not.
+       
+2000-12-16 03:28  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Rights checks for ticket ownership work best when the right that's set is the same
+       as the right that's checked for *sigh*
+       
+2000-12-16 03:26  jesse
+
+       * webrt/Admin/Groups/Members.html:
+
+       Fixed a couple bugs in Group mebership administration code.
+       
+2000-12-16 03:04  jesse
+
+       * webrt/: Admin/Elements/GroupTabs, Admin/Elements/QueueTabs,
+       Admin/Elements/SystemTabs, Admin/Elements/Tabs,
+       Admin/Elements/UserTabs, Admin/Global/index.html,
+       Admin/Groups/Modify.html, Admin/Groups/index.html,
+       Admin/Queues/People.html, Admin/Queues/index.html,
+       Admin/Users/Modify.html, Admin/Users/index.html,
+       Elements/SelectWatcherType, Elements/Tabs, Ticket/Create.html,
+       Ticket/Display.html, Ticket/Elements/Tabs:
+
+       Bugfix in user admin (no longer automatically revokes "privileged status)
+       a bunch of tab-related UI cleanup. the start of tabs that reflect the current page.
+       
+       added the ability to configure queue watchers from the web ui
+       
+2000-12-16 03:04  jesse
+
+       * webrt/Admin/Queues/People.html:
+
+       file People.html was initially added on branch rt-1-1.
+       
+2000-12-16 00:58  jesse
+
+       * webrt/Admin/Users/index.html:
+
+       typo fixing.
+       
+2000-12-15 19:12  jesse
+
+       * webrt/Admin/Global/GroupRights.html:
+
+       file GroupRights.html was initially added on branch rt-1-1.
+       
+2000-12-15 19:12  jesse
+
+       * webrt/Admin/Global/UserRights.html:
+
+       file UserRights.html was initially added on branch rt-1-1.
+       
+2000-12-15 19:12  jesse
+
+       * webrt/Admin/Elements/GroupTabs:
+
+       file GroupTabs was initially added on branch rt-1-1.
+       
+2000-12-15 19:12  jesse
+
+       * lib/RT/ACE.pm, lib/RT/Group.pm, lib/RT/Groups.pm,
+       lib/RT/Record.pm, lib/RT/Scrip.pm, lib/RT/ScripAction.pm,
+       lib/RT/Ticket.pm, lib/RT/Transaction.pm, lib/RT/User.pm,
+       lib/RT/Users.pm, tools/insertdata, webrt/Admin/Elements/GroupTabs,
+       webrt/Admin/Elements/SystemTabs, webrt/Admin/Global/ACL.html,
+       webrt/Admin/Global/GroupRights.html,
+       webrt/Admin/Global/UserRights.html,
+       webrt/Admin/Groups/Members.html, webrt/Admin/Groups/Modify.html,
+       webrt/Admin/Groups/index.html, webrt/Admin/Users/Modify.html,
+       webrt/Admin/Users/index.html:
+
+       A bunch of cleanup work on Users and Groups (and a couple of small bugfixes, one
+       which might prevent install of Alpha 2) mostly aimed at improving the admin ui.
+       
+       We now have two levels of "Privileged" for a user, 1 which means "can be granted rights"
+       and 2 which means "should not be futzed with by the user"
+       
+2000-12-14 23:04  jesse
+
+       * webrt/Ticket/Attachment/dhandler:
+
+       Accidentally left out of the last rev. sorry about that.
+       
+2000-12-14 23:04  jesse
+
+       * webrt/Ticket/Attachment/dhandler:
+
+       file dhandler was initially added on branch rt-1-1.
+       
+2000-12-12 20:44  jesse
+
+       * Makefile:
+
+       Bumped version to 1.3.26. This is RT2 Alpha 2, folks.
+       
+2000-12-12 20:34  jesse
+
+       * lib/RT/Attachment.pm, lib/RT/Record.pm, lib/RT/Ticket.pm,
+       lib/RT/Transaction.pm, lib/RT/User.pm,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Attachment display in the webui now just works [tm] RT2 Alpha 2 will be rolled later tonight.
+       
+2000-12-12 19:03  jesse
+
+       * webrt/Admin/Global/Template.html:
+
+       file Template.html was initially added on branch rt-1-1.
+       
+2000-12-12 19:03  jesse
+
+       * webrt/Admin/Global/Templates.html:
+
+       file Templates.html was initially added on branch rt-1-1.
+       
+2000-12-12 19:03  jesse
+
+       * README, bin/webmux.pl, lib/RT/Attachment.pm, lib/RT/Template.pm,
+       lib/RT/Transaction.pm, webrt/Admin/Elements/SelectTemplate,
+       webrt/Admin/Elements/SystemTabs, webrt/Admin/Global/Template.html,
+       webrt/Admin/Global/Templates.html,
+       webrt/Admin/Queues/Templates.html,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Working on attachments support.
+       
+       did some work on global templates
+       
+2000-12-12 15:15  jesse
+
+       * lib/RT/Template.pm, lib/RT/Templates.pm, tools/insertdata:
+
+       Core support for system-scoped templates
+       
+2000-12-12 14:12  jesse
+
+       * webrt/Admin/: Queues/Modify.html, Users/Create.html,
+       Users/Modify.html:
+
+       Fixed queue and user creation problems.
+       
+2000-12-12 03:05  jesse
+
+       * webrt/Ticket/: Link.html, ModifyLinks.html, Elements/ShowSummary,
+       Elements/Tabs:
+
+       Brought the still basic link editing ui into conformance with the rest of the ticket ui
+       
+2000-12-12 03:05  jesse
+
+       * webrt/Ticket/ModifyLinks.html:
+
+       file ModifyLinks.html was initially added on branch rt-1-1.
+       
+2000-12-12 02:41  jesse
+
+       * webrt/Ticket/: ModifyDates.html, ModifyPeople.html:
+
+       split out the ticket modify screen into 3 more bite-sized pages.
+       
+2000-12-12 02:41  jesse
+
+       * webrt/Ticket/ModifyPeople.html:
+
+       file ModifyPeople.html was initially added on branch rt-1-1.
+       
+2000-12-12 02:41  jesse
+
+       * webrt/Ticket/ModifyDates.html:
+
+       file ModifyDates.html was initially added on branch rt-1-1.
+       
+2000-12-12 02:41  jesse
+
+       * webrt/Ticket/: Modify.html, Elements/Tabs:
+
+       [no log message]
+       
+2000-12-11 15:01  jesse
+
+       * webrt/NoAuth/Login.html:
+
+       file Login.html was initially added on branch rt-1-1.
+       
+2000-12-11 15:01  jesse
+
+       * webrt/NoAuth/Logout.html:
+
+       file Logout.html was initially added on branch rt-1-1.
+       
+2000-12-11 15:01  jesse
+
+       * webrt/: Login.html, Logout.html, autohandler, Elements/Footer,
+       Elements/Header, NoAuth/Login.html, NoAuth/Logout.html:
+
+       Moving Login and Logout around so they work right with the new NoAuth scheme
+       
+2000-12-11 05:00  jesse
+
+       * webrt/: Login.html, autohandler:
+
+       fixed a couple login bugs. implemented some logic to deal with different auth classes:
+       /NoAuth/ and /SelfService/
+       
+2000-12-11 01:46  jesse
+
+       * webrt/: rt.jpg, Elements/Header, Elements/MyRequests,
+       Elements/MyTickets, Search/Listing.html, Ticket/Create.html,
+       Ticket/Create_Detail.html, Ticket/Display.html, Ticket/Modify.html,
+       Ticket/Update.html, Ticket/Elements/ShowTransaction,
+       Ticket/Elements/Tabs, Ticket/Elements/TicketToolBox:
+
+        Work on ticket display. we're getting there.
+       
+2000-12-10 23:53  jesse
+
+       * webrt/Admin/Queues/Template.html:
+
+       file Template.html was initially added on branch rt-1-1.
+       
+2000-12-10 23:53  jesse
+
+       * webrt/Admin/Queues/Templates.html:
+
+       file Templates.html was initially added on branch rt-1-1.
+       
+2000-12-10 23:53  jesse
+
+       * webrt/Admin/Elements/QueueTabs:
+
+       file QueueTabs was initially added on branch rt-1-1.
+       
+2000-12-10 23:53  jesse
+
+       * webrt/Admin/Elements/SystemTabs:
+
+       file SystemTabs was initially added on branch rt-1-1.
+       
+2000-12-10 23:53  jesse
+
+       * webrt/Admin/: Global/Scrips.html, Queues/Scrips.html:
+
+       file Scrips.html was initially added on branch rt-1-1.
+       
+2000-12-10 23:53  jesse
+
+       * webrt/Admin/Elements/UserTabs:
+
+       file UserTabs was initially added on branch rt-1-1.
+       
+2000-12-10 23:53  jesse
+
+       * webrt/Admin/: Global/ACL.html, Queues/ACL.html:
+
+       file ACL.html was initially added on branch rt-1-1.
+       
+2000-12-10 23:53  jesse
+
+       * webrt/Admin/Global/index.html:
+
+       file index.html was initially added on branch rt-1-1.
+       
+2000-12-10 23:53  jesse
+
+       * Makefile, webrt/index.html, webrt/Admin/ModifyUser,
+       webrt/Admin/index.html, webrt/Admin/Elements/Header,
+       webrt/Admin/Elements/QueueTabs, webrt/Admin/Elements/SystemTabs,
+       webrt/Admin/Elements/Tabs, webrt/Admin/Elements/UserTabs,
+       webrt/Admin/Global/ACL.html, webrt/Admin/Global/Scrips.html,
+       webrt/Admin/Global/index.html, webrt/Admin/Queues/ACL.html,
+       webrt/Admin/Queues/Modify.html, webrt/Admin/Queues/Scrips.html,
+       webrt/Admin/Queues/Template.html,
+       webrt/Admin/Queues/Templates.html, webrt/Admin/Queues/index.html,
+       webrt/Admin/Users/Modify.html, webrt/Admin/Users/index.html,
+       webrt/Elements/Header, webrt/Elements/Tabs:
+
+       Bunch of work on the web administration framework.
+       Lots of stuff should be much cleaner now.
+       
+2000-12-10 01:21  jesse
+
+       * Makefile:
+
+       Bumped version to 1.3.25 for release
+       
+2000-12-10 01:15  jesse
+
+       * webrt/: Elements/TitleBoxEnd, Elements/TitleBoxStart,
+       SelfService/Elements/MyRequests, SelfService/Elements/Tabs,
+       Ticket/Update.html, User/Prefs.html:
+
+       Work on SSRI and a bit of overall UI tweaking
+       
+2000-12-09 22:48  jesse
+
+       * webrt/SelfService/Elements/Header:
+
+       file Header was initially added on branch rt-1-1.
+       
+2000-12-09 22:48  jesse
+
+       * webrt/SelfService/Elements/: Header, MyRequests, Tabs:
+
+       Missed a couple files for SSRI
+       
+2000-12-09 22:48  jesse
+
+       * webrt/SelfService/Elements/MyRequests:
+
+       file MyRequests was initially added on branch rt-1-1.
+       
+2000-12-09 22:48  jesse
+
+       * webrt/SelfService/Elements/Tabs:
+
+       file Tabs was initially added on branch rt-1-1.
+       
+2000-12-09 22:43  jesse
+
+       * webrt/SelfService/Create.html:
+
+       file Create.html was initially added on branch rt-1-1.
+       
+2000-12-09 22:43  jesse
+
+       * webrt/SelfService/Details.html:
+
+       file Details.html was initially added on branch rt-1-1.
+       
+2000-12-09 22:43  jesse
+
+       * webrt/SelfService/index.html:
+
+       file index.html was initially added on branch rt-1-1.
+       
+2000-12-09 22:43  jesse
+
+       * webrt/SelfService/Closed.html:
+
+       file Closed.html was initially added on branch rt-1-1.
+       
+2000-12-09 22:43  jesse
+
+       * webrt/Elements/SelectNewTicketQueue:
+
+       file SelectNewTicketQueue was initially added on branch rt-1-1.
+       
+2000-12-09 22:43  jesse
+
+       * webrt/Admin/Groups/Members.html:
+
+       file Members.html was initially added on branch rt-1-1.
+       
+2000-12-09 22:43  jesse
+
+       * webrt/Admin/Elements/SelectUsers:
+
+       file SelectUsers was initially added on branch rt-1-1.
+       
+2000-12-09 22:43  jesse
+
+       * lib/RT/CurrentUser.pm, lib/RT/Group.pm, lib/RT/GroupMember.pm,
+       lib/RT/Record.pm, lib/RT/User.pm, lib/RT/Users.pm,
+       webrt/Login.html, webrt/autohandler,
+       webrt/Admin/Elements/SelectModifyGroup,
+       webrt/Admin/Elements/SelectUsers, webrt/Admin/Groups/Members.html,
+       webrt/Admin/Groups/Modify.html, webrt/Admin/Groups/index.html,
+       webrt/Admin/Users/Modify.html, webrt/Admin/Users/index.html,
+       webrt/Elements/Header, webrt/Elements/SelectNewTicketQueue,
+       webrt/SelfService/Closed.html, webrt/SelfService/Create.html,
+       webrt/SelfService/Details.html, webrt/SelfService/index.html,
+       webrt/User/Prefs.html:
+
+       First rev of SSRI, the Self Service Requestor Interface.
+       Did a bunch of work on groups and the admin UI
+       
+2000-12-05 23:24  jesse
+
+       * webrt/Admin/Groups/Modify.html:
+
+       file Modify.html was initially added on branch rt-1-1.
+       
+2000-12-05 23:24  jesse
+
+       * webrt/Admin/Groups/index.html:
+
+       file index.html was initially added on branch rt-1-1.
+       
+2000-12-05 23:24  jesse
+
+       * webrt/Admin/Elements/SelectModifyGroup:
+
+       file SelectModifyGroup was initially added on branch rt-1-1.
+       
+2000-12-05 23:24  jesse
+
+       * lib/RT/Action/NotifyAsComment.pm:
+
+       file NotifyAsComment.pm was initially added on branch rt-1-1.
+       
+2000-12-05 23:24  jesse
+
+       * lib/RT/Group.pm, lib/RT/GroupMembers.pm, lib/RT/Scrip.pm,
+       lib/RT/Action/Notify.pm, lib/RT/Action/NotifyAsComment.pm,
+       lib/RT/Action/SendEmail.pm, tools/insertdata,
+       webrt/Admin/Elements/SelectModifyGroup,
+       webrt/Admin/Groups/Modify.html, webrt/Admin/Groups/index.html,
+       webrt/Admin/Queues/Modify.html, webrt/Admin/Queues/index.html:
+
+       Lots of work on groups. I think the core is basically set.  UI has been started
+       
+       Scrips got even more work and more code. they now work at least as well as
+       they ever have _and_ they now do it for the right reasons.
+       
+2000-12-02 13:44  jesse
+
+       * Makefile:
+
+       
+       fixed a local customization in the makefile
+       
+2000-12-02 13:34  jesse
+
+       * Makefile:
+
+       bumped version to 1.0.6
+       
+2000-12-02 03:22  jesse
+
+       * webrt/Ticket/Link.html:
+
+       file Link.html was initially added on branch rt-1-1.
+       
+2000-12-02 03:22  jesse
+
+       * lib/RT/Scrip.pm, lib/RT/ScripActions.pm, lib/RT/Scrips.pm,
+       webrt/Ticket/Link.html:
+
+       catching some stragglers.
+       
+2000-12-02 03:22  jesse
+
+       * lib/RT/ScripActions.pm:
+
+       file ScripActions.pm was initially added on branch rt-1-1.
+       
+2000-12-02 03:20  jesse
+
+       * webrt/Admin/Elements/SelectScripAction:
+
+       file SelectScripAction was initially added on branch rt-1-1.
+       
+2000-12-02 03:20  jesse
+
+       * webrt/Admin/Elements/SelectScripCondition:
+
+       file SelectScripCondition was initially added on branch rt-1-1.
+       
+2000-12-02 03:20  jesse
+
+       * lib/RT/: Action/Generic.pm, Condition/Generic.pm:
+
+       file Generic.pm was initially added on branch rt-1-1.
+       
+2000-12-02 03:20  jesse
+
+       * lib/RT/Condition/AnyTransaction.pm:
+
+       file AnyTransaction.pm was initially added on branch rt-1-1.
+       
+2000-12-02 03:20  jesse
+
+       * lib/RT/Condition/NewDependency.pm:
+
+       file NewDependency.pm was initially added on branch rt-1-1.
+       
+2000-12-02 03:20  jesse
+
+       * lib/RT/ScripAction.pm:
+
+       file ScripAction.pm was initially added on branch rt-1-1.
+       
+2000-12-02 03:20  jesse
+
+       * lib/RT/ScripCondition.pm:
+
+       file ScripCondition.pm was initially added on branch rt-1-1.
+       
+2000-12-02 03:20  jesse
+
+       * lib/RT/ScripConditions.pm:
+
+       file ScripConditions.pm was initially added on branch rt-1-1.
+       
+2000-12-02 03:20  jesse
+
+       * bin/webmux.pl, docs/design_docs/subscription-definitions.txt,
+       etc/schema.pm, lib/RT/Action.pm, lib/RT/Scrip.pm,
+       lib/RT/ScripAction.pm, lib/RT/ScripCondition.pm,
+       lib/RT/ScripConditions.pm, lib/RT/ScripScope.pm,
+       lib/RT/ScripScopes.pm, lib/RT/Scrips.pm, lib/RT/Transaction.pm,
+       lib/RT/Action/Generic.pm, lib/RT/Action/Notify.pm,
+       lib/RT/Action/OpenDependent.pm, lib/RT/Action/ResolveMembers.pm,
+       lib/RT/Action/SendEmail.pm, lib/RT/Action/SendEmailOnResolve.pm,
+       lib/RT/Action/StallDependent.pm,
+       lib/RT/Condition/AnyTransaction.pm, lib/RT/Condition/Generic.pm,
+       lib/RT/Condition/NewDependency.pm, tools/insertdata,
+       webrt/Admin/Elements/SelectScripAction,
+       webrt/Admin/Elements/SelectScripCondition:
+
+       Major major major work on scrips
+       scrips now specify a 'Stage' which has only one value for now but should be easier to add in other
+       points in the code where they get called
+       
+       What used to be called Scrip got split into ScripCondition and ScripAction
+       
+       ScripScope got renamed 'Scrip', like it should have been from the getgo.
+       
+       Did some work to the scrip mailing routines.All in all, scrips are much
+       cleaner and more flexible. and at least as functional as they were last night.
+       
+       I'll be adjusting the templates as I get to them
+       
+2000-12-01 14:02  jesse
+
+       * NEWS:
+
+       fixed a display bug in how we split messages for the webui
+       
+2000-11-30 02:18  jesse
+
+       * lib/RT/Action/AutoReply.pm, lib/RT/Action/SendEmail.pm,
+       tools/insertdata:
+
+       Working on cleaning up the Scrips system.  The whole templating system
+       was really, really incredibly overdesigned and badly implemented on the first go-round.
+       Over the next few checkins, I'll be cleaning it up and cleaning it out...
+       
+2000-11-29 16:31  jesse
+
+       * bin/testdeps.pl:
+
+       updated apache::session dependency
+       
+2000-11-29 01:32  jesse
+
+       * lib/RT/ACE.pm, lib/RT/ScripScope.pm, lib/RT/Ticket.pm,
+       lib/RT/Transaction.pm, lib/RT/User.pm, lib/RT/Action/AutoReply.pm,
+       lib/RT/Action/SendEmail.pm, lib/RT/Interface/Email.pm,
+       webrt/Admin/Elements/SelectScrip,
+       webrt/Admin/Elements/SelectTemplate,
+       webrt/Ticket/Create_Detail.html:
+
+       Work on scrips all around:
+               web ui cleanups
+               Action::SendEmail cleanups
+       
+       work on acls for Ticket creation.
+       now users who only have "CreateTicket" can actually create tickets,
+       even if they can't see the ticket once created.
+       RT1's "allow nonmembers to create requests" can be replicated by
+       granting  the metagroup 'requestors' the right "CreateTicket".
+       
+       Mail gateway got a lot of debugging stubs. mail gateway now uses
+       modern semantics for Create.
+       
+2000-11-28 17:46  jesse
+
+       * lib/RT/User.pm:
+
+       cacheed acl decisions are now properly granular.
+       
+2000-11-28 17:39  jesse
+
+       * lib/RT/User.pm:
+
+       Cached ACL decisions are now expire after 2 minutes.
+       
+2000-11-28 03:37  jesse
+
+       * etc/config.pm, lib/RT/Ticket.pm, webrt/Ticket/Update.html,
+       webrt/Ticket/Elements/ShowReferences,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       fixed a small bug in ticket update.
+       fixed a set of bugs in "external" links that were discovered when my roommate suggested
+       that RT could be used as an mp3 playlist server.  Now it can. no, you really don't want to.
+       At least until we have asset managment.
+       
+2000-11-28 00:20  jesse
+
+       * Makefile, lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       lib/RT/Action/SendEmail.pm:
+
+       a couple little tweask to transaction and sendemail.
+       
+       bumped the version #
+       
+2000-11-28 00:12  jesse
+
+       * webrt/Ticket/: Display.html, LinkIt.html, ProcessUpdate.html,
+       Update.html, Elements/ShowSummary, Elements/ShowTransaction,
+       Elements/TicketToolBox:
+
+       Work on cleaning up the web ui. got rid of processupdate.html.
+       cleaned up the code in display.html..  linking is no longer nearly as offensive.
+       
+2000-11-27 20:26  jesse
+
+       * webrt/Ticket/Elements/ShowRequestor:
+
+       small display fix to not show nobody as a possible requestor
+       
+2000-11-27 20:05  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/User.pm, webrt/Ticket/Display.html:
+
+       Updates to how Create handles Requestor, Cc and Admin Cc.
+       it's now much more flexible, cleaner and simpler. yay.
+       
+2000-11-27 04:01  jesse
+
+       * webrt/Elements/: Error, Header, MyRequests:
+
+       Web error reporting tweaks.
+       added MyRequests to the front page
+       
+2000-11-27 04:01  jesse
+
+       * webrt/Elements/MyRequests:
+
+       file MyRequests was initially added on branch rt-1-1.
+       
+2000-11-27 03:30  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/Interface/Web.pm,
+       webrt/Ticket/Display.html, webrt/Ticket/Elements/ShowHistory:
+
+       more work on ACLs
+       
+       Ticket.pm has been fully ACLed.
+       Did work on web ticket create.  it's no longer a festering pile of garbage.
+       Now it's more of an office wastepaper basket full of clean white 20lb paper.
+       Seriously, though, it's cleaner. I got rid of some of Tobias' temporary ticket creation
+       code.
+       
+2000-11-26 23:45  jesse
+
+       * lib/RT/ACE.pm, lib/RT/GroupMember.pm, lib/RT/Queue.pm,
+       lib/RT/Queues.pm, lib/RT/Template.pm, lib/RT/Ticket.pm,
+       lib/RT/User.pm, lib/RT/Interface/Web.pm, tools/insertdata,
+       webrt/Admin/Elements/ModifyQueue, webrt/Elements/Header,
+       webrt/Elements/Tabs:
+
+       
+       
+       lots more ACL work.
+       a little bit of UI cleanup.
+       
+2000-11-25 01:56  jesse
+
+       * lib/RT/Tickets.pm, webrt/index.html,
+       webrt/Ticket/Elements/ShowDates:
+
+       made "my requests" doable.
+       fixed some ui in showdates.
+       
+2000-11-24 20:14  jesse
+
+       * lib/RT/: Queue.pm, User.pm:
+
+       Fixed a couple of bugs in HasSystemRight that make RT install correctly again.
+       
+2000-11-24 19:21  jesse
+
+       * etc/schema.pm:
+
+       schema fix to make pg happy
+       
+2000-11-22 03:08  jesse
+
+       * webrt/Ticket/Update.html:
+
+       cleaned up the ticket udpdate form. made it more concise.::
+       
+2000-11-22 03:04  jesse
+
+       * webrt/Ticket/Update.html:
+
+       cleaning up the ticket update form
+       
+2000-11-22 02:49  jesse
+
+       * bin/testdeps.pl:
+
+       bumped searchbuilder dependency to the version now in CPAN...
+       
+2000-11-22 02:31  jesse
+
+       * Makefile, lib/RT/ACE.pm, lib/RT/ACL.pm, lib/RT/Action.pm,
+       lib/RT/Attachment.pm, lib/RT/Attachments.pm, lib/RT/CurrentUser.pm,
+       lib/RT/Date.pm, lib/RT/EasySearch.pm, lib/RT/Group.pm,
+       lib/RT/GroupMember.pm, lib/RT/GroupMembers.pm, lib/RT/Groups.pm,
+       lib/RT/Handle.pm, lib/RT/Link.pm, lib/RT/Links.pm, lib/RT/Queue.pm,
+       lib/RT/Queues.pm, lib/RT/Record.pm, lib/RT/Scrip.pm,
+       lib/RT/ScripScope.pm, lib/RT/ScripScopes.pm, lib/RT/Scrips.pm,
+       lib/RT/Template.pm, lib/RT/Templates.pm, lib/RT/Ticket.pm,
+       lib/RT/Tickets.pm, lib/RT/Transaction.pm, lib/RT/Transactions.pm,
+       lib/RT/User.pm, lib/RT/Users.pm, lib/RT/Utils.pm,
+       lib/RT/Watcher.pm, lib/RT/Watchers.pm:
+
+       Bumped version to 1.3.23
+       Added pod headers to most core modules?
+       
+2000-11-21 23:05  jesse
+
+       * webrt/Ticket/Elements/ShowSummary:
+
+       removed a dead link
+       
+2000-11-21 04:07  jesse
+
+       * Makefile, lib/RT/Queue.pm, lib/RT/Scrip.pm, lib/RT/ScripScope.pm,
+       lib/RT/Transaction.pm, lib/RT/User.pm:
+
+       tmp logfiles now go in /tmp
+       oh. and SCRIPS NOW WORK. RT can send mail.
+       and recieve mail. life is good.
+       
+2000-11-21 02:55  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Implemented Ticket->IsWatcher.
+       This is what we needed to make pseudogroup based ACLs work
+       
+2000-11-21 00:55  jesse
+
+       * lib/RT/Queue.pm, lib/RT/Ticket.pm, lib/RT/User.pm,
+       webrt/Elements/Tabs:
+
+       fixed a couple of bugs while I was out and about.
+       Oh. and most of the rest of the ACL core is implemented now.
+       rights for requestor/owner/cc/admincc should now work.
+       
+2000-11-20 11:59  jesse
+
+       * lib/RT/ACE.pm, lib/RT/User.pm, tools/insertdata:
+
+       Added "Requestor" and "Everyone" metagroups to tools/insertdata
+       
+       Fixed a bug with yet another way to call HasQueueRight.
+       
+2000-11-20 00:39  jesse
+
+       * Makefile:
+
+       bumped to 1.3.22 for release
+       
+2000-11-20 00:24  jesse
+
+       * webrt/Search/Listing.html:
+
+       fixed a big triggered when clearing empty ticket searches
+       
+2000-11-19 23:55  jesse
+
+       * bin/webmux.pl, lib/RT/User.pm:
+
+       fixed a bug that stopped you from specifying an owner on ticket create
+       Watcher and Watchers are now preloaded in the App server.
+       
+2000-11-19 23:20  jesse
+
+       * webrt/Ticket/Elements/Tabs:
+
+       file Tabs was initially added on branch rt-1-1.
+       
+2000-11-19 23:20  jesse
+
+       * webrt/: index.html, Admin/Elements/Tabs, Elements/Header,
+       Elements/Tabs, Search/Listing.html, Search/RestrictSearch.html,
+       Ticket/Create.html, Ticket/Create_Detail.html, Ticket/Display.html,
+       Ticket/EditWatchers.html, Ticket/Modify.html, Ticket/Update.html,
+       Ticket/Elements/Tabs:
+
+       more UI tweaking. yay.
+       
+2000-11-19 17:57  jesse
+
+       * webrt/Elements/Quicksearch:
+
+       file Quicksearch was initially added on branch rt-1-1.
+       
+2000-11-19 17:57  jesse
+
+       * webrt/Elements/MyTickets:
+
+       file MyTickets was initially added on branch rt-1-1.
+       
+2000-11-19 17:57  jesse
+
+       * webrt/rt.jpg:
+
+       file rt.jpg was initially added on branch rt-1-1.
+       
+2000-11-19 17:57  jesse
+
+       * lib/RT/ACE.pm, lib/RT/Ticket.pm, lib/RT/Tickets.pm,
+       lib/RT/Interface/Web.pm, webrt/index.html, webrt/rt.jpg,
+       webrt/Elements/MyTickets, webrt/Elements/Quicksearch,
+       webrt/Search/Listing.html:
+
+       A bunch of UI work. we now have a placeholder logo
+       
+2000-11-18 03:03  jesse
+
+       * lib/RT/: User.pm, Interface/Web.pm:
+
+       ACL  change messages are now properly scoped. so now they don't hang out
+       between sessions
+       
+       if a user has a system right, that right now applies to all queues.
+       
+2000-11-17 00:19  jesse
+
+       * Makefile:
+
+       Rolled rev 1.3.21. now with an ACL editor.
+       
+2000-11-17 00:15  jesse
+
+       * webrt/Admin/Elements/SelectRights:
+
+       file SelectRights was initially added on branch rt-1-1.
+       
+2000-11-17 00:15  jesse
+
+       * lib/RT/ACE.pm, lib/RT/Group.pm, lib/RT/Queue.pm,
+       lib/RT/Template.pm, lib/RT/Ticket.pm, lib/RT/User.pm,
+       lib/RT/Interface/Web.pm, webrt/Admin/Elements/SelectRights:
+
+       Woo! the ACL editor is now much cleaner. and it works. :)
+       
+2000-11-16 01:17  jesse
+
+       * lib/RT/: ACE.pm, ACL.pm, Attachment.pm, Attachments.pm,
+       CurrentUser.pm, EasySearch.pm, Group.pm, GroupMember.pm,
+       GroupMembers.pm, Groups.pm, Link.pm, Links.pm, Queue.pm, Queues.pm,
+       Record.pm, Scrip.pm, ScripScope.pm, ScripScopes.pm, Scrips.pm,
+       Template.pm, Templates.pm, Ticket.pm, Transaction.pm,
+       Transactions.pm, User.pm, Users.pm, Watcher.pm, Watchers.pm,
+       Interface/Web.pm:
+
+       
+       LEANED UP A WHOLE BUNCH OF NEW ROUTINES. FIXED A FREW ACL BUGS.
+       
+2000-11-15 02:29  jesse
+
+       * lib/RT/ACE.pm, lib/RT/ACL.pm, lib/RT/CurrentUser.pm,
+       lib/RT/Queue.pm, lib/RT/Ticket.pm, lib/RT/User.pm,
+       webrt/Admin/Elements/Tabs, webrt/Elements/Submit:
+
+       refactored ACLS to take out a layer of complexity or two.
+       
+2000-11-14 13:17  jesse
+
+       * Makefile:
+
+       bumped the makefuile version
+       
+2000-11-13 23:19  jesse
+
+       * bin/webmux.pl, lib/RT/ACL.pm, lib/RT/EasySearch.pm,
+       lib/RT/Group.pm, lib/RT/GroupMember.pm, lib/RT/Record.pm,
+       lib/RT/User.pm:
+
+       A bunch of refactoring to make ACL editable. Lots of work on the ACL editor.
+       Basic user ACLs are now editable.
+       It needs some more refactoring, since I stopped being on quite as much crack
+       over the wekeend and figured out a much less intense way to do a bunch of stuff. Yay.
+       
+2000-11-08 14:55  jesse
+
+       * Makefile, NEWS:
+
+       all mail sent out is now sent out precedence "bulk" like it should be.
+       
+2000-11-08 01:24  jesse
+
+       * webrt/Admin/Elements/SelectScrip:
+
+       file SelectScrip was initially added on branch rt-1-1.
+       
+2000-11-08 01:24  jesse
+
+       * webrt/Admin/Elements/SelectTemplate:
+
+       file SelectTemplate was initially added on branch rt-1-1.
+       
+2000-11-08 01:24  jesse
+
+       * etc/schema.pm, lib/RT/ACE.pm, lib/RT/ACL.pm,
+       lib/RT/CurrentUser.pm, lib/RT/Group.pm, lib/RT/Ticket.pm,
+       lib/RT/Tickets.pm, lib/RT/Transaction.pm, lib/RT/User.pm,
+       lib/RT/Users.pm, tools/insertdata,
+       webrt/Admin/Elements/SelectScrip,
+       webrt/Admin/Elements/SelectTemplate:
+
+       A whole slew of work on ACLs and assorted other related things.
+       This involved a lot of cleanup of ACL related code and things it touched.
+       ACL decisions are now being made. (Yes, you're all still superusers) but I
+       think I've got a bunch of the infrastructure cleaned up, so it should be
+       easier to finish off the ACL editor. yay!
+       
+       As part of this, I had to add more groups support. all you groups-maniacs should be pleased ;)
+       
+2000-11-06 11:55  jesse
+
+       * Makefile:
+
+       Fixed yet another typo in manipulate.pm.
+       
+       Bumped version to 1.0.5
+       
+2000-11-05 15:24  jesse
+
+       * Makefile:
+
+       A fix for action vs actions in mail manipulate.
+       fixed a typo normailize -> normalize in database/manipulate.pm
+       
+2000-11-03 17:54  jesse
+
+       * README, etc/schema.pm, lib/RT/ACE.pm, lib/RT/ACL.pm,
+       lib/RT/CurrentUser.pm, lib/RT/GroupMember.pm, lib/RT/Queue.pm,
+       lib/RT/Ticket.pm, lib/RT/User.pm, lib/RT/Users.pm,
+       lib/RT/Interface/Email.pm, lib/RT/Interface/Web.pm,
+       tools/insertdata, webrt/Admin/ModifyUser,
+       webrt/Admin/Elements/QueueRightsForUser, webrt/Admin/Elements/Tabs,
+       webrt/Elements/SelectOwner:
+
+       A bunch of work on the ACLs. we're getting closer to having a workable ACL
+       editor.
+       
+2000-11-03 15:37  jesse
+
+       * Makefile, NEWS:
+
+       We now deal better with merging merged tickets.
+       
+       We now properly ignore Precedence: {junk|bulk} headers
+       
+2000-10-31 00:06  jesse
+
+       * lib/RT/: ACE.pm, ScripScope.pm:
+
+       Added ACL support to the ScripScope system.
+       
+2000-10-29 21:31  jesse
+
+       * Makefile, README, bin/webmux.pl, lib/RT/ACE.pm, lib/RT/Group.pm,
+       lib/RT/Groups.pm, lib/RT/Queue.pm, lib/RT/ScripScope.pm,
+       lib/RT/ScripScopes.pm, lib/RT/Scrips.pm, lib/RT/Template.pm,
+       lib/RT/Templates.pm, lib/RT/Transaction.pm, tools/insertdata:
+
+       A bunch of hacking to the ScripScopes system.
+       
+       You can now edit scrips for a given queue.
+       but hey, scrips have no ACL checking yet.
+       
+2000-10-29 17:51  jesse
+
+       * etc/schema.pm:
+
+       Added a whole lot of documentation to schema.pm.
+       
+2000-10-25 21:09  jesse
+
+       * Makefile, bin/testdeps.pl, etc/schema.pm:
+
+       Updated schema.pm and testdeps to jibe with the current CPAN versions of things.
+       And I bumped the version to 1.3.20
+       
+2000-10-23 16:53  jesse
+
+       * Makefile, lib/RT/Date.pm:
+
+       Reverted to using mysql by default.
+       Finished off the postgresql support in the rt core. (well, at least finished the initial support)
+       
+2000-10-23 16:35  jesse
+
+       * Makefile, lib/RT/ACE.pm, lib/RT/Ticket.pm, tools/insertdata:
+
+       Cleanups related to making postgres support work right.
+       
+2000-10-22 20:57  jesse
+
+       * Makefile, bin/initacls.Pg, bin/testdeps.pl, etc/acl.Pg,
+       etc/config.pm, etc/schema.pm, lib/RT/Link.pm, lib/RT/Ticket.pm,
+       lib/RT/Tickets.pm, tools/initdb, tools/insertdata,
+       webrt/Ticket/Display.html:
+
+       merged in ivan's postgres patches.
+       made local ticket links work again. *sigh* SQL evil. eeeeevil.
+       
+2000-10-16 16:32  jesse
+
+       * docs/Security:
+
+       file Security was initially added on branch rt-1-1.
+       
+2000-10-16 16:32  jesse
+
+       * docs/Security:
+
+       some initial notes on security. targetted at RT admins.
+       
+2000-10-16 03:01  jesse
+
+       * lib/RT/: Link.pm, Links.pm, Ticket.pm:
+
+       Fixed a couple of typos in link and links.
+       Ticket->Load will now do the right hting with ticket uris or aliases.
+       
+2000-10-16 00:47  jesse
+
+       * etc/config.pm, lib/RT/Link.pm, lib/RT/Links.pm, lib/RT/Ticket.pm:
+
+       The linking interface now uses URIs internally. and it does lots of
+       sanity checking.
+       
+       Tickets now understand what their uris should be. you can load tickets by
+       uri. and by alias.
+       
+       We need a bit more work to make alias support just transparent, but we're
+       getting really close.
+       
+       You shouldn't be able to link to nonexistent local objects any mroe.
+       
+2000-10-15 01:57  jesse
+
+       * lib/RT/CurrentUser.pm:
+
+       Added CurrentUser->LoadByGecos.
+       The CLI now uses LoadByGecos to load the currentuser.
+       This means that users other than root might actually be able to use the cli now
+       VS: ----------------------------------------------------------------------
+       
+2000-10-15 01:11  jesse
+
+       * Makefile:
+
+       Significantly redid the installation procedure. we're now _much_
+       more careful about what gets installed where and what's owned by whom.
+       
+       Oh. and RT's now setgid, rather than setuid. and there's no setuid wrapper anymore
+       
+2000-10-15 01:08  jesse
+
+       * etc/: config.pm, suidrt.c:
+
+       Yanked suidrt.c, since we now run setgid.
+       
+       rt now logs to /tmp/rt.log.pid.userid
+       
+2000-10-14 02:56  jesse
+
+       * bin/rtmux.pl, bin/webmux.pl, etc/schema.pm,
+       lib/RT/CurrentUser.pm, lib/RT/Interface/Email.pm, tools/initdb,
+       tools/insertdata:
+
+       CurrentUser.pm had LoadByEmail and LoadByUserId methods added and they're now
+       actually used most everywhere.
+       
+       insertdata doesn't force ids for users now.
+       
+       the schema now actually enforces a lot of important uniqueness constraints.
+       
+2000-10-13 10:59  jesse
+
+       * etc/schema.mysql:
+
+       Removed the old schema.mysql, lest it lead people astray
+       
+2000-10-13 02:27  jesse
+
+       * webrt/Ticket/Elements/: EditPeople, ShowMembers:
+
+       Removed some old warning text that's not true any more.
+       Fixed a relative url problem in "show memebers"
+       
+2000-10-12 23:22  jesse
+
+       * Makefile, bin/testdeps.pl:
+
+       updated testdeps.
+       Makefile now defaults to installing rt2 in /opt/rt2
+       
+2000-10-12 22:54  jesse
+
+       * Makefile, lib/RT/ACL.pm, lib/RT/Queue.pm, lib/RT/Ticket.pm,
+       lib/RT/User.pm, lib/RT/Users.pm, tools/insertdata,
+       webrt/index.html:
+
+       A couple of links on the front page.
+       Makefile now assumes www-data instead of nobody as the web user. this is not quite right.
+       
+       Fixed a little bit of the POD in User.pm.
+       
+       Redid how Ticket.pm deals with Owner on create. the new logic should actually catch errors
+       instead of easily letting referential integrity checks just _fail_.
+       
+       Insertdata got cleaned up a little bit.
+       
+       ACL got its cleaned up a bit
+       
+2000-10-11 23:22  jesse
+
+       * webrt/Admin/Elements/QueueRightsForUser:
+
+       file QueueRightsForUser was initially added on branch rt-1-1.
+       
+2000-10-11 23:22  jesse
+
+       * lib/RT/Users.pm, webrt/Admin/Elements/QueueRightsForUser:
+
+       Work on ACLs. and the ACL editor
+       
+2000-10-11 21:23  jesse
+
+       * tools/insertdata:
+
+       gave root a password
+       
+2000-10-11 12:28  jesse
+
+       * Makefile, tools/initdb:
+
+       Databasename changed from RT2 to rt2 to make postgres happier.
+       
+       initdb quoting bug fixed.
+       
+       debug mode in initdb turned off.
+       
+       If you're using mysql and running with ivan's current CVS version of DBIx::DBSchema, RT should once again work.
+       
+2000-10-09 02:32  jesse
+
+       * etc/schema.pm, tools/initdb:
+
+       a debugging hook in initdb and defaults (though they don't work just right yet)
+       in schema.pm. Note that we now need DBIx::DBSchema from CVS.
+       
+2000-10-09 01:59  jesse
+
+       * Makefile, NEWS, etc/suidrt.c:
+
+       Jan Kujawa fixed a bug in the setuid wrapper
+       Jan Okrouhly fixed some bugs in the merged ticket resolution in the cli.
+       
+       Rolled 1.0.5pre3
+       
+2000-10-05 17:30  jesse
+
+       * lib/RT/: ACE.pm, ACL.pm:
+
+       more work on RT's acl core
+       
+2000-10-05 17:30  jesse
+
+       * webrt/Admin/Elements/: GrantQueueRightsTo, GrantQueueRightsTo~,
+       SelectQueueRights:
+
+       more acl work
+       
+2000-10-05 17:30  jesse
+
+       * webrt/Admin/Elements/GrantQueueRightsTo~:
+
+       file GrantQueueRightsTo~ was initially added on branch rt-1-1.
+       
+2000-10-05 17:30  jesse
+
+       * webrt/Admin/Elements/GrantQueueRightsTo:
+
+       file GrantQueueRightsTo was initially added on branch rt-1-1.
+       
+2000-10-05 17:30  jesse
+
+       * webrt/Admin/Elements/SelectQueueRights:
+
+       file SelectQueueRights was initially added on branch rt-1-1.
+       
+2000-10-05 15:49  jesse
+
+       * etc/suidrt.c:
+
+       reordering things in suidrt.c seemed to make them happier.
+       
+2000-10-05 15:39  jesse
+
+       * etc/suidrt.c:
+
+       missed a comma
+       
+2000-10-05 15:03  jesse
+
+       * bin/testdeps.pl:
+
+       added a dependency test script to make installation by newbies easier.
+       
+2000-10-05 15:03  jesse
+
+       * Makefile, README, etc/suidrt.c:
+
+       Major rewrite of suidrt.c by jan kujawa.
+       A couple of bugfixes from Jan Okrouhly
+               Public history should now work right in the cli
+               Web viewing of merged tickets by their old # should now work better.
+       
+2000-10-03 20:22  jesse
+
+       * bin/testdeps.pl:
+
+       cleaned up testedeps.pl output
+       
+2000-10-03 02:41  jesse
+
+       * tools/insertdata:
+
+       file insertdata was initially added on branch rt-1-1.
+       
+2000-10-03 02:41  jesse
+
+       * tools/initdb:
+
+       file initdb was initially added on branch rt-1-1.
+       
+2000-10-03 02:41  jesse
+
+       * etc/schema.pm:
+
+       file schema.pm was initially added on branch rt-1-1.
+       
+2000-10-03 02:41  jesse
+
+       * Makefile, bin/initdb.Oracle, bin/initdb.Pg, bin/initdb.mysql,
+       bin/testdeps.pl, etc/schema.Pg, etc/schema.pm, lib/RT/ACE.pm,
+       lib/RT/Queue.pm, lib/RT/Scrip.pm, lib/RT/User.pm, tools/initdb,
+       tools/insertdata:
+
+       Fairly massive installation changes.
+               We now use ivan's really cool DBIx::DBschema, which, when things
+       settle out a bit mean that the oracle and postgres (and possibly other) ports
+       get their schema updated automatically.
+       
+       The initial seed data is now inserted by tools/insertdata through the RT API.
+       
+       ACE::Create now actually works.
+       Same with Scrip::Create.
+       and Queue::Create.
+       
+       There are a couple of new installation-only dependencies. One of them (DBSchema) may become a build-only
+       dependency if people whine enough :)
+       
+       date/time handling was a casualty of the changes. some things will be handled oddly for now.
+       Once Ivan releases the next DBSchema update, this should get better again. it was
+       the result of a namespace collision between pg and mysql. the timestamp column has
+       different behavior. go fig.
+       
+       This version will require DBIx::SearchBuilder 0.06 (aka what I'm about to check in)
+       
+2000-10-03 02:07  jesse
+
+       * tools/test:
+
+       blew away old, crufty "extras"
+       
+2000-09-28 13:55  jesse
+
+       * lib/RT/Group.pm:
+
+       file Group.pm was initially added on branch rt-1-1.
+       
+2000-09-28 13:55  jesse
+
+       * lib/RT/: Group.pm, GroupMember.pm, GroupMembers.pm, Groups.pm:
+
+       Long overdue adding of completely untested (and unused) code for groups in RT.
+       note that this implementation does not assume recursive group membership
+       
+2000-09-28 13:55  jesse
+
+       * lib/RT/GroupMember.pm:
+
+       file GroupMember.pm was initially added on branch rt-1-1.
+       
+2000-09-28 13:55  jesse
+
+       * lib/RT/GroupMembers.pm:
+
+       file GroupMembers.pm was initially added on branch rt-1-1.
+       
+2000-09-28 13:55  jesse
+
+       * lib/RT/Groups.pm:
+
+       file Groups.pm was initially added on branch rt-1-1.
+       
+2000-09-18 01:57  jesse
+
+       * lib/RT/: ACE.pm, Queue.pm, Ticket.pm:
+
+       Lots of work on Queue.pm  Most cleanups related to queue watchers, but I
+       also added a bit more documentation and fixed a bug that could cause DelWatcher
+       in ticket.pm to delete watchers it shouldn't
+       
+2000-09-18 00:03  jesse
+
+       * docs/API, docs/FAQ, docs/README.oracle, lib/RT/Ticket.pm,
+       lib/RT/Transaction.pm:
+
+       More documentation. removed outdated docs.
+       
+       docs/API now talks about what those of you writing your own RT client
+       code shouldn't be doing. (Which objects you shouldn't touch).
+       
+2000-09-18 00:00  jesse
+
+       * README:
+
+       Clarified license terms. RT is available under Version 2 of the GPL.
+       Not version 1. Not some as-yet-unwritten version 3 that says you can only
+       use it if you agree to license your children under the GPL. Version 2.
+       
+2000-09-17 19:57  jesse
+
+       * lib/RT/: TicketCollection.pm, User.pm, Interface/Email.pm:
+
+       Removed bogus signature code.
+       Documented User->IsPassword
+       removed --area flag from Mailgateway (We ain't got no stinking areas)
+       
+2000-09-17 19:21  jesse
+
+       * etc/schema.Oracle, etc/schema.mysql, lib/RT/ACE.pm,
+       lib/RT/ACL.pm, lib/RT/Scrip.pm, lib/RT/User.pm:
+
+       Ugh. Mysql isn't respecting SQL92 reserved words. which meant that I didn't
+       notice that I was using "Type" and "Action" in my schema.
+       This required a bit of churn to the ACE and User modules.
+       
+2000-09-17 17:38  jesse
+
+       * Makefile:
+
+       Ok. I think I've got it now. This is RT 1.3.18. aka RT2 - Alpha 1.
+               The "Bear Suit" Release.
+       
+               A formal release announcement is forthcoming.
+       
+2000-09-17 17:29  jesse
+
+       * Makefile:
+
+       work on the changelog generator
+       
+2000-09-17 17:19  jesse
+
+       * HACKING, Makefile, README, bin/initdb.mysql, bin/testdeps.pl:
+
+       Bumped the required version of SearchBuilder in testdeps, now that it's
+       propagated throughout CPAN
+       
+       Replaced initdb.mysql with ivan's new version.
+       
+       Tweaked ivan's initdb.mysql to be a little friendlier, create the schema
+       (it was missing a FILEHANDLE in a print statement and deal
+       better with omitted passwords.
+       
+       Updated the readme some more.
+       
+       Added experimental ChangeLog generation to the make dist process
+       
+       Bumped the version number to 1.3.18 for release as alpha1 for RT2 today.
+       
+2000-09-17 01:40  jesse
+
+       * Makefile, README, bin/initacls.mysql, bin/testdeps.pl,
+       bin/webmux.pl:
+
+       Bumped the Mason version requirement up, so we avoid the poisoned v 0.88
+       Applied ivan's alpha-1 patches.
+       Did some tweaking for the alpha 1 release.
+       Cleaned up the readme a bit
+       
+2000-09-15 01:21  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+2000-09-15 01:17  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       Mail gateway now handles followup correspondence properly
+       (It gets the ticket # right)
+       
+2000-09-15 01:06  jesse
+
+       * lib/RT/Watcher.pm:
+
+       fixed a typo (left off a > ) in Watcher.pm
+       
+2000-09-15 00:59  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/Watcher.pm, webrt/Ticket/Modify.html:
+
+       Editing ticket watchers from the webui now works.
+       this required a bunch of work on the internal wathers stuff
+       in ticket.pm.
+       Also added documentation and watcher-related sanity checks
+       
+2000-09-14 00:04  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/Watcher.pm,
+       webrt/Elements/SelectWatcherType, webrt/Ticket/Elements/EditPeople,
+       webrt/Ticket/Elements/EditWatchers,
+       webrt/Ticket/Elements/ModifyTicket:
+
+       Some cleanup to Watcher and Ticket. (mainly documentation updates)
+       Added an IsUser sub to Watcher.pm (which tells you if the watcher
+       object refers to a local user or a remote email address)
+       
+       the ui for editing tickets should work now.
+       note that the backend for the watchers side of this isn't there
+       yet.
+       
+               -j
+       
+2000-09-13 18:10  jesse
+
+       * etc/config.pm:
+
+       fixed a configfile typo that would break a new installation
+       
+2000-09-12 01:28  jesse
+
+       * lib/RT/Date.pm, lib/RT/Ticket.pm, webrt/Elements/ListActions,
+       webrt/Elements/SelectDate, webrt/Elements/SelectUsers,
+       webrt/Elements/SelectWatcherType, webrt/Ticket/Modify.html,
+       webrt/Ticket/Elements/AddWatchers, webrt/Ticket/Elements/EditDates,
+       webrt/Ticket/Elements/EditPeople,
+       webrt/Ticket/Elements/ModifyTicket:
+
+       TimeWorked is now read/write (which may be a bad idea. but I'm willing to try it.
+       RT::Date now better understands that "never" doesn't mean 1970.
+       
+       The web modify interface is getting closer to working.
+       I mainly need to finish making the watchers column go.
+       
+2000-09-11 00:37  jesse
+
+       * etc/config.pm:
+
+       Some sanity cleanups to the web queue listing.
+       
+2000-09-11 00:35  jesse
+
+       * webrt/Ticket/Elements/EditDates:
+
+       file EditDates was initially added on branch rt-1-1.
+       
+2000-09-11 00:35  jesse
+
+       * webrt/Ticket/Elements/EditWatchers:
+
+       file EditWatchers was initially added on branch rt-1-1.
+       
+2000-09-11 00:35  jesse
+
+       * webrt/Ticket/Elements/AddWatchers:
+
+       file AddWatchers was initially added on branch rt-1-1.
+       
+2000-09-11 00:35  jesse
+
+       * webrt/Ticket/Elements/EditBasics:
+
+       file EditBasics was initially added on branch rt-1-1.
+       
+2000-09-11 00:35  jesse
+
+       * webrt/Ticket/Elements/EditPeople:
+
+       file EditPeople was initially added on branch rt-1-1.
+       
+2000-09-11 00:35  jesse
+
+       * webrt/Ticket/Elements/ModifyTicket:
+
+       file ModifyTicket was initially added on branch rt-1-1.
+       
+2000-09-11 00:35  jesse
+
+       * webrt/: Login.html, webrt.css, Elements/SelectDate,
+       Elements/SelectMatch, Elements/SelectQueue, Elements/SelectUsers,
+       Elements/ShadedBox, Ticket/EditWatchers.html, Ticket/Modify.html,
+       Ticket/Elements/AddWatchers, Ticket/Elements/EditBasics,
+       Ticket/Elements/EditDates, Ticket/Elements/EditPeople,
+       Ticket/Elements/EditWatcherList, Ticket/Elements/EditWatchers,
+       Ticket/Elements/ModifyTicket, Ticket/Elements/ShowBasics,
+       Ticket/Elements/ShowDates, Ticket/Elements/ShowDependencies,
+       Ticket/Elements/ShowHistory, Ticket/Elements/ShowPeople,
+       Ticket/Elements/ShowSummary, Ticket/Elements/TicketToolBox:
+
+       Lots and lots of work on the webui.
+       The display UI has been cleaned up a bit and the modify UI has been started.
+       It's not in its final for yet, nor is there any logic backing many of the new ui features, but those will come next.
+       
+       If I'm remebering my list correctly, this is the one "biggie" before Alpha 1.
+       
+       Yay!
+       
+2000-09-11 00:35  jesse
+
+       * webrt/Elements/SelectUsers:
+
+       file SelectUsers was initially added on branch rt-1-1.
+       
+2000-09-07 00:52  jesse
+
+       * webrt/: Login.html, autohandler, Ticket/Elements/ShowSummary:
+
+       Replaced tobias' web arg preservation code with something that's actually based on _mason_ rather than the external apache object. This should make the fastcgi port easier
+       
+       Fixed a bug in showsummary (unqualified WebPath)
+       
+2000-09-07 00:30  jesse
+
+       * Makefile, bin/rtmux.pl, lib/RT/Handle.pm:
+
+       Look ma! it should install again (i'd flubbed a bit of the
+       fastcgi mason handler install.
+       
+       Oh. and oracle support should work now.
+       
+2000-09-06 00:52  jesse
+
+       * webrt/Logout.html:
+
+       Logout.html links you to the right place now
+       
+2000-09-05 23:47  jesse
+
+       * webrt/Search/Listing.html:
+
+       YA typo fix
+       
+2000-09-05 23:45  jesse
+
+       * webrt/Search/Listing.html:
+
+       Damn I wish I could type tonight. :/ missed an $RT::
+       
+2000-09-05 23:43  jesse
+
+       * webrt/: Search/Listing.html, Ticket/Elements/ShowSummary:
+
+       Fixed a few more Absolute url bugs
+       
+2000-09-05 23:35  jesse
+
+       * webrt/Elements/Tabs:
+
+       Tabs needed / as the final character for transparent proxying
+       
+2000-09-05 22:39  jesse
+
+       * webrt/: Login.html, Admin/Elements/CreateUserCalled,
+       Admin/Elements/ModifyQueue, Admin/Elements/ModifyTemplate,
+       Admin/Elements/ModifyUser, Admin/Users/index.html:
+
+       more work on proper absolute pathing
+       
+2000-09-05 22:08  jesse
+
+       * webrt/: Login.html, Admin/Elements/CreateQueueCalled,
+       Admin/Elements/CreateUserCalled, Admin/Elements/ModifyUser,
+       Admin/Users/index.html:
+
+       A bunch of the admin tools weren't properly dealing with absolute pathed requests. it made it impossible to have RT2 anywhere other than at the / of your webserver
+       
+2000-09-05 21:40  jesse
+
+       * lib/RT/Handle.pm:
+
+       file Handle.pm was initially added on branch rt-1-1.
+       
+2000-09-05 21:40  jesse
+
+       * bin/mason_handler.fcgi:
+
+       file mason_handler.fcgi was initially added on branch rt-1-1.
+       
+2000-09-05 21:40  jesse
+
+       * Makefile, bin/mason_handler.fcgi, bin/testdeps.pl,
+       lib/RT/Handle.pm:
+
+       Updated testdeps to ask for the new version of mailtools
+       Added in the new fastcgi handler (not working yet)
+       and RT::Handle, which is a wrapper for SearchBuilder::Handle
+       
+2000-09-04 22:52  jesse
+
+       * Makefile, TODO, bin/rtmux.pl, bin/webmux.pl, etc/config.pm,
+       lib/RT/Record.pm, lib/RT/User.pm, lib/RT/Interface/Web.pm,
+       webrt/Ticket/Display.html, webrt/Ticket/Elements/ShowRequestor,
+       webrt/Ticket/Elements/TicketToolBox:
+
+       A couple of bugfixes related to the switch to SearchBuilder.
+       A few webui cleanups.
+       A bit of abstraction to make the eventual fastcgi port easier.
+       
+2000-09-04 12:48  jesse
+
+       * bin/testdeps.pl, lib/RT/Attachments.pm, lib/RT/Ticket.pm,
+       lib/RT/Transaction.pm, lib/RT/Watcher.pm, lib/RT/Watchers.pm,
+       lib/RT/Interface/Web.pm, webrt/Ticket/Elements/ShowDates:
+
+       Several batched updates from when pallas was off-net
+       
+       UpdateTold changed to SetTold.
+       A bunch of work to get Scrips working.
+       Lots more POD in Ticket.pm
+       Transaction->Describe  is better about printing what really happened.
+       Attachments.pm had a typo that prevented it from dealing with multipart messages.
+       
+2000-08-31 02:18  jesse
+
+       * Makefile, NEWS, etc/suidrt.c:
+
+       Added ENV squashing to suidrt.c
+       
+2000-08-30 14:46  jesse
+
+       * bin/rtmux.pl:
+
+       Rolling in some oracle changes...they're not done yet, but nothing should
+       break with mysql. fixed a typo in rtmux.pl that was introduced with the switch to searchbuilder.
+       
+2000-08-29 17:02  jesse
+
+       * README, bin/rtmux.pl, bin/testdeps.pl, bin/webmux.pl,
+       lib/RT/EasySearch.pm, lib/RT/Record.pm:
+
+       MAJOR CHANGE: Switched to the new name of the DBIx:: modules.
+       We now use DBIx::SearchBuilder rather than DBIx::EasySearch and friends.
+       Note that this is only a name and structure change for the module set.
+       The functionality is the same...though seperating out oracle and mysql
+       specific features comes soon.
+       
+2000-08-29 16:57  jesse
+
+       * etc/schema.Oracle:
+
+       updated the schema.Oracle
+       
+2000-08-29 02:02  jesse
+
+       * webrt/: Elements/SelectWatcherType, Ticket/EditWatchers.html,
+       Ticket/Update.html:
+
+       Started hacking on watchers and ticket update webui a bit.
+       they need a lot more work
+       
+2000-08-29 01:51  jesse
+
+       * webrt/Ticket/: Display.html, DisplayHistory, DisplayTransaction,
+       Elements/ShowHistory, Elements/ShowTransaction:
+
+       Made FullHeaders/BriefHeaders work in the webui
+       
+2000-08-28 01:46  jesse
+
+       * webrt/Admin/Elements/: CreateUserCalled, ModifyUser:
+
+       Fixed some display buglets from tobi oetiker
+       
+2000-08-28 01:31  jesse
+
+       * webrt/Ticket/Update.html:
+
+       Removed some text that harassed the user. that's generally bad policy
+       
+2000-08-27 23:56  jesse
+
+       * webrt/: Admin/Elements/Header, Admin/Elements/ModifyTemplate,
+       Admin/Elements/Tabs, Elements/ListActions, Elements/Tabs:
+
+       A few more added helper elements from the webui
+       
+2000-08-27 23:56  jesse
+
+       * webrt/Admin/Elements/Header:
+
+       file Header was initially added on branch rt-1-1.
+       
+2000-08-27 23:56  jesse
+
+       * webrt/: Admin/Elements/Tabs, Elements/Tabs:
+
+       file Tabs was initially added on branch rt-1-1.
+       
+2000-08-27 23:56  jesse
+
+       * webrt/Admin/Elements/ModifyTemplate:
+
+       file ModifyTemplate was initially added on branch rt-1-1.
+       
+2000-08-27 23:56  jesse
+
+       * webrt/Elements/ListActions:
+
+       file ListActions was initially added on branch rt-1-1.
+       
+2000-08-27 23:54  jesse
+
+       * webrt/Admin/Users/Prefs.html:
+
+       file Prefs.html was initially added on branch rt-1-1.
+       
+2000-08-27 23:54  jesse
+
+       * webrt/Admin/: Queues/Create.html, Users/Create.html:
+
+       file Create.html was initially added on branch rt-1-1.
+       
+2000-08-27 23:54  jesse
+
+       * webrt/Admin/: Queues/Modify.html, Users/Modify.html:
+
+       file Modify.html was initially added on branch rt-1-1.
+       
+2000-08-27 23:54  jesse
+
+       * webrt/Admin/: Queues/index.html, Users/index.html:
+
+       file index.html was initially added on branch rt-1-1.
+       
+2000-08-27 23:54  jesse
+
+       * bin/webmux.pl, lib/RT/Area.pm, lib/RT/Areas.pm, lib/RT/Queue.pm,
+       lib/RT/ScripScopes.pm, lib/RT/Template.pm, lib/RT/Templates.pm,
+       webrt/Admin/CreateQueue.html, webrt/Admin/CreateUser.html,
+       webrt/Admin/EditUser.html, webrt/Admin/ModifyQueue.html,
+       webrt/Admin/ModifyUser.html, webrt/Admin/index.html,
+       webrt/Admin/Elements/CreateQueueCalled,
+       webrt/Admin/Elements/CreateUserCalled,
+       webrt/Admin/Elements/ModifyQueue, webrt/Admin/Elements/ModifyUser,
+       webrt/Admin/Elements/SelectModifyQueue,
+       webrt/Admin/Elements/SelectModifyUser,
+       webrt/Admin/Queues/Create.html, webrt/Admin/Queues/Modify.html,
+       webrt/Admin/Queues/index.html, webrt/Admin/Users/Create.html,
+       webrt/Admin/Users/Modify.html, webrt/Admin/Users/Prefs.html,
+       webrt/Admin/Users/index.html:
+
+       Lots of work on the web admin ui. basic user and queue editing working.
+       and template editing
+       
+2000-08-24 15:53  jesse
+
+       * Makefile, README, bin/rtmux.pl, etc/config.pm,
+       lib/RT/Action/SendEmail.pm, webrt/Elements/Footer,
+       webrt/Elements/Header, webrt/Elements/ViewUser,
+       webrt/Ticket/Elements/EditWatcherList:
+
+       The first cut at better configuration.
+       
+       Updated the readme
+       
+       Made the mail send routine in lib/RT/Action/Email.pm somewhat
+       more configurable (though we're still using printing to a pipe
+       because Mail::Mailer is busted :/)
+       
+       Most options moved out of the makefile..this will make packaging
+       possible.
+       
+       Fixed a couple places where tobias had been using a non-relative
+       url unnecessarily.
+       
+2000-08-22 03:08  jesse
+
+       * Makefile:
+
+       Getting version #s in sync for RT 1.3.15
+       
+2000-08-22 03:05  jesse
+
+       * lib/RT/: Scrip.pm, Transaction.pm, Action/AutoReply.pm,
+       Action/SendEmail.pm:
+
+       Work on making sure mail gets sent. It's not "right" yet but it's getting
+       closer.
+       
+2000-08-21 19:46  jesse
+
+       * webrt/autohandler:
+
+       Fixed a minor issue that let people "log in as ''"
+       
+2000-08-21 01:12  jesse
+
+       * bin/rtmux.pl, etc/config.pm, lib/RT/Ticket.pm, lib/RT/Tickets.pm:
+
+       Set a default type in ticket.pm
+       allowed restriction based on type in tickets.pm
+       
+       now set the timezone in the config file rather than the rtmux.
+       means it effects webmux.pl too.
+       
+2000-08-20 23:41  jesse
+
+       * etc/schema.mysql, lib/RT/Date.pm, webrt/Elements/SelectOwner:
+
+       Standardized StartsBy to Starts.
+       Removed a warning from SelectOwner
+       Fixed a bug in RT::Date->Set(Format => 'unknown')
+       
+2000-08-20 01:46  jesse
+
+       * lib/RT/Ticket.pm, webrt/Search/Listing.html,
+       webrt/Ticket/Elements/ShowMemberOf,
+       webrt/Ticket/Elements/ShowMembers:
+
+       Work on MemberOf and Members in Ticket.pm
+       they're both now Tickets objects rather than links objects.
+       and the things that use them have been updated
+       
+2000-08-19 03:03  jesse
+
+       * Makefile:
+
+       Bumped the version 1.3.14
+       
+2000-08-19 02:49  jesse
+
+       * README, etc/config.pm, lib/RT/Ticket.pm:
+
+       Did some work on logging. switched some carping to some logging.
+       
+2000-08-18 02:04  jesse
+
+       * Makefile, README:
+
+       Minor readme updates.
+       
+       Bumped the version to 1.3.13
+       
+2000-08-18 01:04  jesse
+
+       * lib/RT/Transaction.pm:
+
+       CurrentUser objects act on things. not user Objects.  Thanks, Jens
+       
+2000-08-17 23:55  jesse
+
+       * webrt/Elements/: SelectOwner, Submit:
+
+       notes in select owner. a bit of tweaking in submit ot make it more visible
+       
+2000-08-17 16:16  jesse
+
+       * Makefile:
+
+       Changes to rt-mailgate to properly respect authenticated users
+       when performing %RT RESOLVE commands.
+       
+2000-08-17 03:01  jesse
+
+       * lib/RT/Interface/Web.pm:
+
+       Missed a checkin on Web.pm. sorry about that
+       
+2000-08-17 02:53  jesse
+
+       * bin/webmux.pl, lib/RT/Database.pm, lib/RT/Date.pm,
+       lib/RT/Ticket.pm, lib/RT/Interface/Web.pm:
+
+       Yanked the ancient lib/RT/Database.pm. It  never served any purpose
+       added some functionality to RT::Date. it can now take a date type
+       of 'unknown.'  This will "require" Date::Manip and parse it into
+       an ISO style date.  note that this should NEVER be called from RT's
+       core due to overhead. It is useful from web interfaces and CLI tools....
+       
+       added date::manip as a requirement to webmux.pl (so it gets loaded before
+       client hits)
+       
+       cleaned up Interface/Web.pm
+       
+       rationalized some of the routines dealing with date stuff in ticket.pm.
+       
+       Actual working web date changing should be coming "soon." (where soon is
+       defined as sometime this week)
+       
+2000-08-16 14:46  jesse
+
+       * webrt/Elements/Submit:
+
+       file Submit was initially added on branch rt-1-1.
+       
+2000-08-16 14:46  jesse
+
+       * webrt/: Login.html, index.html, Elements/Footer, Elements/Header,
+       Elements/Submit, Ticket/Elements/ShowBasics,
+       Ticket/Elements/ShowTransaction:
+
+       Various bits of webui cleanup
+       
+2000-08-16 14:16  jesse
+
+       * lib/RT/: Tickets.pm, Interface/Email.pm:
+
+       We can now search for tickets by relationship
+       
+2000-08-16 13:18  jesse
+
+       * lib/RT/CurrentUser.pm:
+
+       the real oneline patch that should make rt-mailgate work for new users again.
+       
+2000-08-15 01:17  jesse
+
+       * lib/RT/: Tickets.pm, Interface/Web.pm:
+
+       The first round of convenience methods in RT/Tickets.pm
+       Still need to do the ticket relations methods and the
+       date methods
+       
+2000-08-14 23:47  jesse
+
+       * webrt/Elements/SelectDateType:
+
+       file SelectDateType was initially added on branch rt-1-1.
+       
+2000-08-14 23:47  jesse
+
+       * webrt/Elements/SelectDateType:
+
+       ack. missed this in the wackiness with tonight's earlier checkin.
+       
+2000-08-14 19:35  jesse
+
+       * webrt/: Elements/Header, Elements/SelectDate,
+       Elements/SelectQueue, Elements/TitleBoxStart, Search/Listing.html,
+       Search/PickRestriction:
+
+       The rest of the previous commit.
+       
+2000-08-14 19:27  jesse
+
+       * etc/config.pm, webrt/Login.html, webrt/Logout.html,
+       webrt/autohandler, webrt/index.html, webrt/webrt.css,
+       webrt/Ticket/autohandler, webrt/Ticket/Elements/ShowMemberOf,
+       webrt/Ticket/Elements/ShowMembers,
+       webrt/Ticket/Elements/ShowSummary,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Cleaned up the ticket display a bit.
+       made logout actually properly erase session data
+       protected _everything_ with an autohandler in /
+       prettified Login.html
+       added some options to /Elements/TitleBoxHead...which seems to have been missed
+       
+2000-08-14 14:37  jesse
+
+       * README:
+
+       Added a warning about postmaster from JD
+       
+2000-08-13 21:57  jesse
+
+       * lib/RT/TicketCollection.pm, lib/RT/Tickets.pm,
+       lib/RT/Interface/Web.pm, webrt/Elements/SelectBoolean,
+       webrt/Elements/SelectDate, webrt/Elements/SelectMatch,
+       webrt/Elements/SelectOwner, webrt/Elements/SelectQueue,
+       webrt/Elements/SelectStatus, webrt/Elements/SelectWatcherType,
+       webrt/Search/Listing.html, webrt/Search/PickRestriction,
+       webrt/Search/TicketCell:
+
+       Significant work on the search and display code.
+       I'm not convinced that this doesn't introduce new bugs.
+       However, you can now search by ticket content.
+       
+       I will be reworking Tickets.pm a bit more to add a bunch of convenience methods
+       over then next week or so.
+       
+2000-08-12 18:07  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Fixed a bug in WatchersAsString. (RT::Watchers wasn't required early enough)
+       this whole routine needs to be redone :/
+       
+2000-08-12 18:06  jesse
+
+       * lib/RT/User.pm:
+
+       User->Create CanManipulate now defaults to 0.
+       
+2000-08-12 18:05  jesse
+
+       * etc/schema.mysql:
+
+       Updated the default user entries in schema.mysql to have the "CanManipulate"
+       flag set so they'd show up in owner lists.
+       
+2000-08-10 19:16  jesse
+
+       * README:
+
+       updated instructions for Apache install
+       
+2000-08-10 18:59  jesse
+
+       * etc/schema.mysql:
+
+       the queue values for the first queue were wrong
+       
+2000-08-10 17:55  jesse
+
+       * lib/RT/Ticket.pm:
+
+       BUGFIX: _UpdateTold now doesn't record a transaction, as things should be
+       
+2000-08-10 15:43  jesse
+
+       * etc/config.pm, etc/schema.Oracle, etc/schema.mysql,
+       lib/RT/Attachment.pm, lib/RT/Record.pm, lib/RT/Scrip.pm,
+       lib/RT/ScripScope.pm, lib/RT/Ticket.pm, lib/RT/TicketCollection.pm,
+       lib/RT/Tickets.pm, lib/RT/Transaction.pm, lib/RT/User.pm,
+       lib/RT/Action/AutoReply.pm, lib/RT/Action/Notify.pm,
+       lib/RT/Action/SendEmail.pm, lib/RT/Interface/Web.pm,
+       webrt/autohandler, webrt/Elements/Error,
+       webrt/Elements/SelectStatus, webrt/Ticket/Update.html,
+       webrt/Ticket/Elements/ShowBasics,
+       webrt/Ticket/Elements/ShowDependencies,
+       webrt/Ticket/Elements/ShowMemberOf,
+       webrt/Ticket/Elements/ShowMembers,
+       webrt/Ticket/Elements/ShowPeople,
+       webrt/Ticket/Elements/ShowReferences,
+       webrt/Ticket/Elements/ShowRequestor,
+       webrt/Ticket/Elements/TicketToolBox:
+
+       Fixed ACL caching bugs
+       Finished adding status "new"
+       cli searching based on status works better.
+       
+       API Change. Queue is NO LONGER the Queue Object for a ticket
+       API Change. Owner is NO LONGER the Owner Object for a ticket
+       
+       instead, both point to their proper database values and OwnerObj and QueueObj
+       do the right thing throughout. This was rather more code churn than I was hoping for, but we've now got a cleaner, more consistent API that's easier to work
+       with.
+       
+       little bits of POD update.
+       
+       Cleaned out some unused code.
+       
+       Made some error messages more professional.
+       
+       We now keep track of date started as well as a "start by" date.  these both
+       need a bit more work.
+       
+       Calling convention for _Set changed. Rather than three different calling
+       conventions which weren't very extensible, DBIx::Record::_Set and all its
+       subclasses now use paramhash style calling. it's much more extensible and
+       flexible now. (This was necessary for some ACL work, among other things)
+       
+2000-08-09 01:11  jesse
+
+       * lib/RT/Record.pm, lib/RT/Ticket.pm, lib/RT/User.pm,
+       webrt/Elements/SelectStatus, webrt/Elements/dayMenu,
+       webrt/Elements/monthMenu, webrt/Elements/yearMenu:
+
+       ACL Decisions are now cached
+       date menus have a "never" option in the webui
+       added a new status. "new" for tickets that aren't yet open
+       
+2000-08-08 01:45  jesse
+
+       * etc/schema.mysql, lib/RT/Queue.pm:
+
+       schema updates.
+       fixed queue->Create
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/ModifyQueue.html:
+
+       file ModifyQueue.html was initially added on branch rt-1-1.
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/: CreateQueue.html, CreateUser.html,
+       ModifyQueue.html, ModifyUser.html, index.html,
+       Elements/CreateQueueCalled, Elements/CreateUserCalled,
+       Elements/ModifyQueue, Elements/ModifyUser,
+       Elements/SelectModifyQueue, Elements/SelectModifyUser:
+
+       Started the new web admin interface.
+       it can now edit queues and users and create queues and users
+       I'm fairly leery of its user handling stuff. particularly passwords
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/CreateUser.html:
+
+       file CreateUser.html was initially added on branch rt-1-1.
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/Elements/SelectModifyUser:
+
+       file SelectModifyUser was initially added on branch rt-1-1.
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/Elements/ModifyUser:
+
+       file ModifyUser was initially added on branch rt-1-1.
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/Elements/CreateUserCalled:
+
+       file CreateUserCalled was initially added on branch rt-1-1.
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/ModifyUser.html:
+
+       file ModifyUser.html was initially added on branch rt-1-1.
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/index.html:
+
+       file index.html was initially added on branch rt-1-1.
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/CreateQueue.html:
+
+       file CreateQueue.html was initially added on branch rt-1-1.
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/Elements/ModifyQueue:
+
+       file ModifyQueue was initially added on branch rt-1-1.
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/Elements/SelectModifyQueue:
+
+       file SelectModifyQueue was initially added on branch rt-1-1.
+       
+2000-08-08 01:44  jesse
+
+       * webrt/Admin/Elements/CreateQueueCalled:
+
+       file CreateQueueCalled was initially added on branch rt-1-1.
+       
+2000-08-07 22:28  jesse
+
+       * etc/schema.mysql, lib/RT/Date.pm, lib/RT/Ticket.pm,
+       lib/RT/Users.pm:
+
+       added a few ticket attributes for forwards compatibility
+       fixed another ACL problem in users.pm
+       fixed a date display bug
+       
+2000-08-07 01:03  jesse
+
+       * lib/RT/Transaction.pm, webrt/Ticket/Create.html,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Transaction.pm had some lingering ACL bugs ($CurrentUser) isn't a reasonable
+       global in core library routines :/
+       
+       Working on spawning subtickets.
+       
+2000-08-07 00:31  jesse
+
+       * webrt/User/Prefs.html:
+
+       file Prefs.html was initially added on branch rt-1-1.
+       
+2000-08-07 00:31  jesse
+
+       * webrt/Ticket/Elements/ShowDependencies:
+
+       file ShowDependencies was initially added on branch rt-1-1.
+       
+2000-08-07 00:31  jesse
+
+       * webrt/Ticket/Elements/ShowReferences:
+
+       file ShowReferences was initially added on branch rt-1-1.
+       
+2000-08-07 00:31  jesse
+
+       * webrt/Ticket/Elements/ShowMembers:
+
+       file ShowMembers was initially added on branch rt-1-1.
+       
+2000-08-07 00:31  jesse
+
+       * webrt/Ticket/Elements/ShowMemberOf:
+
+       file ShowMemberOf was initially added on branch rt-1-1.
+       
+2000-08-07 00:31  jesse
+
+       * webrt/Admin/EditUser.html:
+
+       file EditUser.html was initially added on branch rt-1-1.
+       
+2000-08-07 00:31  jesse
+
+       * webrt/Admin/Elements/EditUserComments:
+
+       file EditUserComments was initially added on branch rt-1-1.
+       
+2000-08-07 00:31  jesse
+
+       * etc/config.pm, lib/RT/Ticket.pm, webrt/EditUserComments.html,
+       webrt/ViewUser.html, webrt/webrt.css, webrt/Admin/EditUser.html,
+       webrt/Admin/ModifyUser, webrt/Admin/Elements/EditUserComments,
+       webrt/Elements/Error, webrt/Elements/Header,
+       webrt/Elements/SelectOwner, webrt/Ticket/Create.html,
+       webrt/Ticket/Create_Detail.html, webrt/Ticket/Display.html,
+       webrt/Ticket/Elements/EditWatcherList,
+       webrt/Ticket/Elements/ShowDependencies,
+       webrt/Ticket/Elements/ShowMemberOf,
+       webrt/Ticket/Elements/ShowMembers,
+       webrt/Ticket/Elements/ShowPeople,
+       webrt/Ticket/Elements/ShowReferences,
+       webrt/Ticket/Elements/ShowSummary, webrt/User/Prefs.html:
+
+       Did a bunch of work on the webui. cleaned up a lot of the link display stuff
+       did some work on ticket create
+       
+       dependencies and subtickets are now listed in the ticketview. yay!
+       
+2000-08-05 19:47  jesse
+
+       * lib/RT/Ticket.pm, webrt/Ticket/Elements/ShowSummary:
+
+       more Nobody fixes in Ticket.pm
+       justification fixes in ShowSummary
+       
+2000-08-05 18:50  jesse
+
+       * webrt/Ticket/ProcessUpdate.html:
+
+       The webui can now process updates.
+       it needed YA currentuser fix
+       
+2000-08-05 18:43  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Ticket::Create(Owner =>  now takes either a user object or a userid.
+       and defaults to nobody.
+       
+2000-08-05 17:51  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+2000-08-05 17:49  jesse
+
+       * lib/RT/: Date.pm, Interface/Email.pm:
+
+       fixed a few more bugs in the mailgateway. it can create tickets now.
+       fixed an undefined default in the cli query
+       made the date routine not spit out a stupid warning
+       
+2000-08-05 17:21  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       fixed typos in the mailgate. thanks gary
+       
+2000-08-05 00:16  jesse
+
+       * Makefile, NEWS, webrt/Login.html, webrt/autohandler,
+       webrt/webrt.css:
+
+       little tweaks to the webui.
+       bumped the version to 1.3.11 for distribution
+       
+2000-08-04 18:45  jesse
+
+       * README, bin/rtmux.pl, bin/webmux.pl, lib/RT/Areas.pm,
+       lib/RT/Attachments.pm, lib/RT/Date.pm, lib/RT/Link.pm,
+       lib/RT/Links.pm, lib/RT/Queue.pm, lib/RT/Queues.pm,
+       lib/RT/Record.pm, lib/RT/ScripScopes.pm, lib/RT/Scrips.pm,
+       lib/RT/Template.pm, lib/RT/Ticket.pm, lib/RT/Tickets.pm,
+       lib/RT/Transaction.pm, lib/RT/Transactions.pm, lib/RT/Users.pm,
+       lib/RT/Watchers.pm, lib/RT/Interface/Web.pm,
+       webrt/Admin/ModifyUser, webrt/Elements/SelectOwner,
+       webrt/Elements/SelectQueue, webrt/Elements/ViewUser,
+       webrt/Ticket/Display.html, webrt/Ticket/LinkIt.html,
+       webrt/Ticket/Elements/ShowPeople:
+
+       When creating an object, you ALWAYS need to pass in the current user
+       or acls break. we were a bit too lax about this before.
+       
+       this was a major round of bugfixing for the webui
+       
+2000-08-04 15:41  jesse
+
+       * Makefile, lib/RT/CurrentUser.pm, lib/RT/User.pm, webrt/webrt.css,
+       webrt/Elements/Header, webrt/Search/Listing.html,
+       webrt/Search/autohandler, webrt/Ticket/autohandler:
+
+       Queue listing doesn't have that ugly blue any more. And its code for
+       setting row color is a bit cleaner.
+       
+       we now actually _check_ passwords for web logins.
+       
+       webrt.css now has slightly darker hyperlinks.
+       
+       CurrentUser->IsPassword now uses the UserObj
+       
+       users can't use null passwords for authentication.
+       
+2000-08-03 02:19  jesse
+
+       * lib/RT/Interface/Web.pm, webrt/Elements/ShadedBox,
+       webrt/Elements/TitleBoxStart:
+
+       misc fixes to the webui and a leftover fix to the cli.
+       
+       the webui needs to have some of its internals gutted and
+       put back together. it feels very kludgy and not really "planned"
+       
+2000-08-03 02:04  jesse
+
+       * lib/RT/: Record.pm, Scrip.pm, Ticket.pm, Tickets.pm,
+       Transaction.pm, Action/SendEmail.pm:
+
+       made status changes work. (RT::Action::SendEmail was being stupid
+       and not error checking until it was too late)
+       
+       removed more use of hardwired SQL "now()"
+       
+       moved the handling of LastUpdated into RT::Record. from DBIx::Record
+       
+2000-08-03 01:09  jesse
+
+       * lib/RT/Queue.pm:
+
+       fixed a bug in the cli that kept ticket creates from working.
+       queue->hasright's calling convention changed.
+       
+2000-08-03 00:59  jesse
+
+       * Makefile:
+
+       fixed a makefile typo. added back the comment about pg
+       
+2000-08-03 00:42  jesse
+
+       * docs/design_docs/users:
+
+       file users was initially added on branch rt-1-1.
+       
+2000-08-03 00:42  jesse
+
+       * etc/user.Oracle:
+
+       file user.Oracle was initially added on branch rt-1-1.
+       
+2000-08-03 00:42  jesse
+
+       * etc/schema.Oracle:
+
+       file schema.Oracle was initially added on branch rt-1-1.
+       
+2000-08-03 00:42  jesse
+
+       * bin/initacls.Oracle, bin/initdb.Oracle, docs/README.oracle,
+       docs/design_docs/users, etc/schema.Oracle, etc/user.Oracle:
+
+       A first cut at oracle support from Dave Morgan <dmorgan@bartertrust.com>.
+       It is pretty much untested and guaranteed to break. Among other
+       things, the schema isn't current. but it's a start.
+       Thanks, Dave!
+       
+2000-08-03 00:42  jesse
+
+       * docs/README.oracle:
+
+       file README.oracle was initially added on branch rt-1-1.
+       
+2000-08-03 00:42  jesse
+
+       * bin/initacls.Oracle:
+
+       file initacls.Oracle was initially added on branch rt-1-1.
+       
+2000-08-03 00:42  jesse
+
+       * bin/initdb.Oracle:
+
+       file initdb.Oracle was initially added on branch rt-1-1.
+       
+2000-08-03 00:31  jesse
+
+       * docs/design_docs/local_hacking:
+
+       file local_hacking was initially added on branch rt-1-1.
+       
+2000-08-03 00:31  jesse
+
+       * HACKING, Makefile, docs/FAQ.html, docs/actions.html,
+       docs/admin.html, docs/attributes.html, docs/outline.html,
+       docs/rt_users_guide.html, docs/design_docs/local_hacking:
+
+       doc updates. removed outdated 1.x docs
+       
+2000-08-02 23:53  jesse
+
+       * etc/config.pm:
+
+       added some comments from tobias
+       
+2000-08-02 00:20  jesse
+
+       * Makefile, NEWS, TODO, etc/config.pm, etc/schema.mysql,
+       lib/RT/ACE.pm, lib/RT/ACL.pm, lib/RT/CurrentUser.pm,
+       lib/RT/Date.pm, lib/RT/Queue.pm, lib/RT/Record.pm,
+       lib/RT/Ticket.pm, lib/RT/Tickets.pm, lib/RT/User.pm,
+       lib/RT/Interface/Email.pm:
+
+       
+       Weekend of 1 Aug 2000
+       ---------------------
+       I spent the weekend in DC visiting family.   This meant I got a bit of
+       code written ;)  Sadly, I have no access to the CVS server,
+       so I'll be batching a bunch of commits.
+       
+       1. Enabled CLI admin tool
+       2. Added ACL listing functionality to the CLI admin tool.
+       3. Enhanced RT::Queue->Grant such that it works with the structure of RT2
+          ACLs
+       4. Made the Logging framework actually log errors to STDERR.
+          (This makes debugging the CLI tools much easier. It also means
+           that the cli tools explain _why_ they're dying.)
+       5. Fully expunged use of Mysql's SQL keyword "now()". I'd have left this
+          stuff in, except mysql doesn't seem to deal well with the idea that the
+          entire world isn't one timezone.  On top of that, it doesn't seem to
+          have a way to force it into GMT mode that doesn't involve modifying init
+          scripts. *sigh*
+       6. Did a whole bunch more work on the ACL checking in RT::User
+       7. Wrote up some preliminary docs on local hacks to RT
+       8. Added in a routine to allow local canonicalization of email addresses
+       9. Added in the concept of "Disabled users"  To preserve RT2's database
+          Integrity, whacking user accounts would be a bad thing. So, instead,
+          we've got the concept of 'disabled' users. A disabled user fails ANY
+          ACL check, ANY password check and doesn't appear in any lists of ACLs.
+          (note that the lastmost statement isn't yet true)
+       
+       10. rtadmin user -enable and rtadmin user -disable now work.
+       11. ACLs are now enforced for many ticket related actions.
+           (this does mean that you'll want to insert some acls like those below)
+       
+       INSERT INTO ACL VALUES (1,0,'User','SuperUser','Queue',0);
+       INSERT INTO ACL VALUES (2,3,'User','CreateTicket','Queue',0);
+       INSERT INTO ACL VALUES (3,3,'User','ShowTicket','Ticket',0);
+       INSERT INTO ACL VALUES (4,3,'User','ShowTicketHistory','Ticket',0);
+       INSERT INTO ACL VALUES (6,3,'User','CreateTicket','Queue',1);
+       INSERT INTO ACL VALUES (7,3,'User','ModifyTicket','Ticket',1);
+       INSERT INTO ACL VALUES (8,1,'User','Superuser','System',0);
+       INSERT INTO ACL VALUES (9,0,'Everyone','Superuser','System',0);
+       
+2000-08-02 00:17  jesse
+
+       * bin/: rtmux.pl, testdeps.pl:
+
+       Bumped us up to requiring Log::Dispatch 1.6.
+       Cleaned up testdeps a bit. Now, you knwo it passes.
+       
+2000-07-27 03:01  jesse
+
+       * Makefile:
+
+       Bumped the version to 1.3.9
+       Rolled RT 1.3.9
+       
+2000-07-27 02:37  jesse
+
+       * lib/RT/Date.pm, lib/RT/Ticket.pm, webrt/Elements/ViewUser,
+       webrt/Ticket/Elements/ShowDates, webrt/Ticket/Elements/ShowSummary,
+       webrt/Ticket/Elements/ShowTransaction, bin/webmux.pl:
+
+       Fixed a bug with null due dates.
+       Made RT/Date deal with a time of -1 as "Never"
+       made html escaping on included entities on webrt default.
+       
+2000-07-27 02:01  jesse
+
+       * lib/RT/Date.pm:
+
+       file Date.pm was initially added on branch rt-1-1.
+       
+2000-07-27 02:01  jesse
+
+       * README, lib/RT/Date.pm, lib/RT/Record.pm, lib/RT/Ticket.pm,
+       bin/testdeps.pl:
+
+       Moved Date managment routines from DBIx::Record to RT::Record.
+       Initial Checkin of RT::Date, a lightweight Date object capable
+       of doing everything RT needs. Oh. and it's fully documented in POD.
+       
+       Converted RT::Record and RT::Ticket and the cli to use RT::Date instead
+       of Date::Kronos.  The CLI now feels _much_ zippier and code within
+       RT::Ticket is a bit easier to read.
+       
+2000-07-27 01:55  jesse
+
+       * etc/schema.mysql:
+
+       removed extraneous whitespace
+       
+2000-07-27 01:51  jesse
+
+       * lib/RT/Attachment.pm:
+
+       changed the header on attachment.pm
+       
+2000-07-24 10:17  tobiasb
+
+       * lib/RT/Interface/Web.pm, webrt/ViewUser.html,
+       webrt/Search/TicketCell, webrt/Ticket/Display.html,
+       webrt/Ticket/ProcessUpdate.html, webrt/Ticket/Update.html:
+
+       Moved some things to Web.pm, made some nifty options for getting tickets from the listing in a new window
+       
+2000-07-24 06:56  tobiasb
+
+       * lib/RT/User.pm:
+
+       Some TODOs
+       
+2000-07-24 06:54  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       Some comments and TODOs.
+       
+       I fixed one simple TODO about putting in pid + rand into the name of
+       the temp directory needed for parsing the mime entity.
+       
+2000-07-24 00:52  jesse
+
+       * lib/RT/User.pm:
+
+       Added a few comments where they really should be
+       
+2000-07-24 00:48  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       Did a bunch of cleanup work on Interface/Email.pm
+               It could still use more.
+       
+2000-07-23 03:50  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Added the method 'TimeWorkedAsString'.  Ok, I'll start looking into Date::Kronos shortly :)
+       
+2000-07-22 20:10  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       [no log message]
+       
+2000-07-22 20:05  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       Inserted a reference to [fsck 290] in a TODO-comment.
+       
+2000-07-22 13:06  tobiasb
+
+       * lib/RT/Interface/Web.pm, webrt/Search/Listing.html:
+
+       Moved stuff from Listing.html to Web.pm, sub ProcessSearchQuery (suggestions to a better name for the sub?)
+       
+2000-07-21 15:11  tobiasb
+
+       * lib/RT/Ticket.pm, lib/RT/Interface/Web.pm,
+       webrt/Search/Listing.html:
+
+       bugfixing
+       
+2000-07-21 11:09  tobiasb
+
+       * etc/schema.mysql:
+
+       Bugfix
+       
+2000-07-21 10:57  tobiasb
+
+       * webrt/index.html:
+
+       Now it's possible to select queue from the start page
+       
+2000-07-21 10:35  tobiasb
+
+       * lib/RT/Attachment.pm, lib/RT/Ticket.pm, webrt/Elements/Header,
+       webrt/Elements/SelectOwner, webrt/Ticket/Elements/EditWatcherList:
+
+       bugfixes, comments.  I restricted the Owner-list to those that CanManipulate - this should eventually go away when we have proper access control.  Tiny improvement of EditWatcherList - it's now possible to edit user information if the user is found.
+       
+2000-07-21 05:48  tobiasb
+
+       * lib/RT/CurrentUser.pm:
+
+       bugfix
+       
+2000-07-21 00:08  jesse
+
+       * lib/RT/ACE.pm, lib/RT/Attachment.pm, lib/RT/Queue.pm,
+       lib/RT/Ticket.pm, lib/RT/User.pm, webrt/EditUserComments.html:
+
+       Cleaned up language in EditUserComments
+       
+       did work on watchers in ui/cli/admin and Ticket.pm and User.pm
+       cleaned up code in Ticket.pm
+       
+       Start of some work on acls in Queue.pm
+       
+2000-07-20 22:13  tobiasb
+
+       * lib/RT/Interface/Web.pm, webrt/Elements/SelectWatcherType,
+       webrt/Ticket/Display.html, webrt/Ticket/EditWatchers.html,
+       webrt/Ticket/Elements/EditWatcherList,
+       webrt/Ticket/Elements/ShowHistory, webrt/Ticket/Elements/ShowLinks,
+       webrt/Ticket/Elements/TicketToolBox:
+
+       Some code improvements, added one popular demand here; to avoid spending too much time clicking around, it should be possible to show all histoies of member requests at the same page.
+       
+2000-07-20 18:31  tobiasb
+
+       * lib/RT/CurrentUser.pm:
+
+       Fixing a bit.  Please try to remember to test code before committing
+       it :)
+       
+2000-07-20 18:09  tobiasb
+
+       * webrt/Ticket/ProcessUpdate.html:
+
+       once more I've committed without testing..
+       
+2000-07-20 18:02  tobiasb
+
+       * lib/RT/CurrentUser.pm:
+
+       Removed a compile-time bug
+       
+2000-07-20 17:48  tobiasb
+
+       * lib/RT/User.pm:
+
+       bugfix
+       
+2000-07-20 17:39  tobiasb
+
+       * etc/config.pm, lib/RT/Ticket.pm, lib/RT/User.pm,
+       lib/RT/Watcher.pm, lib/RT/Interface/Web.pm,
+       webrt/Ticket/Display.html:
+
+       bugfixes + conflict resolving at the signature + moved something to Web.pm from Display.html
+       
+2000-07-20 17:31  jesse
+
+       * lib/RT/: CurrentUser.pm, User.pm:
+
+       Culled some not-for-now signature stuff.
+       Cleaned up and optimised CurrentUser->UserObj
+       
+2000-07-20 16:39  jesse
+
+       * Makefile, NEWS:
+
+       doing some work for a customer.  rt-mailgate --help should tell you
+       about the fabulous new mail mode
+       
+2000-07-20 15:39  tobiasb
+
+       * lib/RT/Interface/Web.pm, webrt/Ticket/Display.html:
+
+       moving things from the template to Web.pm was not as straight-forwarded as I had hoped.
+       
+2000-07-20 15:20  tobiasb
+
+       * lib/RT/Attachment.pm, lib/RT/CurrentUser.pm, lib/RT/User.pm,
+       lib/RT/Interface/Web.pm, webrt/Ticket/Display.html:
+
+       Started moving stuff to Web.pm, fixed and tested signatures
+       
+2000-07-20 14:50  tobiasb
+
+       * lib/RT/: CurrentUser.pm, User.pm:
+
+       moved signature from currentuser to user (but does it make sense, anyway?  Signatures is something 'personal' that belongs only to the current user?)
+       
+2000-07-20 14:43  tobiasb
+
+       * webrt/ViewUser.html:
+
+       bugfix ... hmpf, always test before committing .. always test before committing ...
+       
+2000-07-20 14:33  tobiasb
+
+       * lib/RT/Interface/Web.pm, webrt/ViewUser.html:
+
+       bugfix
+       
+2000-07-20 14:11  tobiasb
+
+       * lib/RT/Interface/Web.pm:
+
+       file Web.pm was initially added on branch rt-1-1.
+       
+2000-07-20 14:11  tobiasb
+
+       * lib/RT/Interface/Web.pm:
+
+       I'm considering to move things from Display.html and ProcessUpdate.html to Web.pm.  Comments?
+       
+2000-07-20 14:06  jesse
+
+       * lib/RT/Watcher.pm:
+
+       Removed the bogus code frol lib/RT/Watcher.pm
+       
+2000-07-20 13:39  jesse
+
+       * lib/RT/User.pm:
+
+       reversed some bogus code
+       
+2000-07-20 11:44  tobiasb
+
+       * webrt/Ticket/Elements/EditWatcherList:
+
+       file EditWatcherList was initially added on branch rt-1-1.
+       
+2000-07-20 11:44  tobiasb
+
+       * webrt/Elements/SelectWatcherType:
+
+       file SelectWatcherType was initially added on branch rt-1-1.
+       
+2000-07-20 11:44  tobiasb
+
+       * webrt/: Elements/SelectWatcherType,
+       Ticket/Elements/EditWatcherList:
+
+       Now it's possible to edit ticket and queue watchers.  It's really horrible, but at least it works
+       
+2000-07-20 11:43  tobiasb
+
+       * webrt/Ticket/: EditWatchers.html, Update.html:
+
+       Small comment
+       
+2000-07-20 11:40  tobiasb
+
+       * webrt/Ticket/Update.html:
+
+       Linked in the 'edit watchers' functionality
+       
+2000-07-20 11:27  tobiasb
+
+       * lib/RT/Ticket.pm, lib/RT/Watcher.pm,
+       webrt/Ticket/EditWatchers.html:
+
+       Now it's possible to edit ticket and queue watchers.  It's really horrible, but at least it works
+       
+2000-07-20 11:27  tobiasb
+
+       * lib/RT/User.pm:
+
+       bugfix
+       
+2000-07-20 11:23  tobiasb
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       This is sort of a local hack, I guess it should be separated ... but maybe not.  if content-type =~ /^text/(?\!plain)/, there is now a link for 'view this as plain text', which might be useful for unknown text formats and for viewing 'code'
+       
+2000-07-20 02:59  jesse
+
+       * lib/RT/: ACE.pm, ACL.pm:
+
+       I'm committing ACE.pm and ACL.pm so people can get a bit of a taste of what I'm doing. They don't work, they probably don't even compile yet, but they're not getting called from the rest of the code yet.
+       
+       These aren't the droids you're looking for.
+       
+2000-07-20 02:58  jesse
+
+       * lib/RT/User.pm:
+
+       updated User.pm to match the schema
+       
+2000-07-19 16:45  tobiasb
+
+       * webrt/Ticket/EditWatchers.html:
+
+       file EditWatchers.html was initially added on branch rt-1-1.
+       
+2000-07-19 16:45  tobiasb
+
+       * webrt/Ticket/EditWatchers.html:
+
+       This should be a template for adding and removing watchers; will be completed tomorrow
+       
+2000-07-19 16:10  tobiasb
+
+       * webrt/: Logout.html, ViewUser.html, Search/Listing.html,
+       Ticket/Display.html, Ticket/Update.html,
+       Ticket/Elements/ShowPeople, Ticket/Elements/ShowSummary:
+
+       It's now possible to edit user data (realname, userid, email) from the web ui
+       
+2000-07-19 16:04  tobiasb
+
+       * lib/RT/: CurrentUser.pm, Ticket.pm, Watcher.pm,
+       Interface/Email.pm:
+
+       Worked a bit with requestors; 1) if we already have a user object at the requestor, we should use it.  2) it should be possible to edit the users email without also updating the watcher email if those are the same.  3) if a user enters an email with a different from address, but can be identified as a previous user (by equal real name or other means of authentication), the email field in the watcher table should be used
+       
+2000-07-19 10:06  tobiasb
+
+       * webrt/Elements/Header:
+
+       bugfixing
+       
+2000-07-19 08:53  tobiasb
+
+       * webrt/webrt.css:
+
+       Our web designer didn't like red table borders
+       
+2000-07-19 08:52  tobiasb
+
+       * webrt/: Elements/Header, Search/Listing.html:
+
+       Fun #440: Popular wish: Fixed alternating colours at the lines in Listing.html
+       
+2000-07-19 07:41  tobiasb
+
+       * webrt/webrt.css:
+
+       oddline for use in Listing.html
+       
+2000-07-19 06:54  tobiasb
+
+       * webrt/Elements/Header:
+
+       nicer
+       
+2000-07-19 06:51  tobiasb
+
+       * webrt/: index.html, Elements/Header:
+
+       Fun #666; Form 'view bug #xxx' from display and the front page
+       
+2000-07-18 04:17  tobiasb
+
+       * etc/schema.mysql:
+
+       bug #286; two nines in schema.sql
+       
+2000-07-14 11:40  tobiasb
+
+       * webrt/Search/TicketCell:
+
+       I'm currently trying to fix it so that each user can put in options in the session information (or should it rather be in the DB?) about whether they want tickets to pop up in separate windows or not.
+       
+2000-07-14 10:47  tobiasb
+
+       * etc/schema.mysql, lib/RT/Link.pm, lib/RT/Ticket.pm,
+       lib/RT/Action/StallDependent.pm,
+       webrt/Ticket/Elements/TicketToolBox:
+
+       added a scrip for stalling members upon membership linking ... now it's impossible for a request to make a link to itself ... now it's possible to open a request from the ticket display
+       
+2000-07-14 09:29  tobiasb
+
+       * webrt/Ticket/ProcessUpdate.html:
+
+       TODO: Public comments are currently beeing threated as private
+       comments (that's better than it beeing ignored ... at least at the
+       moment)
+       
+       Now it skips trying to build a MIME entity if there are no message.
+       
+2000-07-14 09:11  tobiasb
+
+       * webrt/ViewUser.html:
+
+       Bugfix
+       
+2000-07-14 09:09  tobiasb
+
+       * webrt/Search/Listing.html:
+
+       Killed a warning
+       
+2000-07-13 13:31  tobiasb
+
+       * webrt/index.html:
+
+       Typo
+       
+2000-07-13 11:44  tobiasb
+
+       * webrt/Elements/Header:
+
+       huh?  seems like I've lost the control of CVS a bit today :)
+       
+2000-07-13 11:43  tobiasb
+
+       * webrt/Elements/Header:
+
+       [no log message]
+       
+2000-07-13 11:36  jesse
+
+       * webrt/index.html:
+
+       tobix missed a &>
+       
+2000-07-13 11:08  tobiasb
+
+       * webrt/: Search/RestrictSearch.html, Ticket/Display.html,
+       Ticket/ProcessUpdate.html, Ticket/Update.html:
+
+       Dealt with the footer; it does not need overriding anywhere, and is thus placed in the autohandler
+       
+2000-07-13 11:04  tobiasb
+
+       * webrt/: Logout.html, ViewUser.html, autohandler, index.html,
+       Elements/Header, Search/Listing.html, Ticket/Modify.html,
+       Ticket/ValidateUpdate.html:
+
+       Dealt with the header module; it's now in the .html templates, not in autohandler ... the title should be nice and informative in all the templates ... and the instance name is viewed along with the header
+       
+2000-07-13 07:26  tobiasb
+
+       * lib/RT/Link.pm, lib/RT/Ticket.pm, webrt/index.html,
+       webrt/Elements/SelectResultsPerPage, webrt/Search/Listing.html,
+       webrt/Search/PickRestriction:
+
+       small bugfix + better handling of LimitResultsOnPage
+       
+2000-07-12 14:09  tobiasb
+
+       * lib/RT/: Ticket.pm, Transaction.pm:
+
+       bugfix'es.  duplicated linking actions are now turned down.
+       
+2000-07-12 01:39  jesse
+
+       * etc/schema.mysql:
+
+       updated the schema for User objects. now they've got _even more_ data.
+       and better extension capabilities.
+       
+2000-07-12 00:10  tobiasb
+
+       * lib/RT/Attachment.pm, lib/RT/Scrip.pm, lib/RT/ScripScope.pm,
+       lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       lib/RT/Action/SendEmail.pm, lib/RT/Action/StallDependent.pm,
+       webrt/Elements/SelectResultsPerPage, webrt/Elements/ViewUser,
+       webrt/Ticket/Create.html, webrt/Ticket/Elements/ShowTransaction:
+
+       Debugged a bit and removed some critical bugs
+       
+2000-07-11 14:43  tobiasb
+
+       * webrt/Elements/ViewUser:
+
+               <% $User->Comments || "No comment entered about this user" |h %>
+       
+       "|h" seems like some random noise, or have I misunderstood something?
+       
+2000-07-11 12:19  tobiasb
+
+       * etc/schema.mysql:
+
+       Comments
+       
+2000-07-11 11:30  tobiasb
+
+       * lib/RT/Transaction.pm, lib/RT/Action/AutoReply.pm,
+       webrt/Logout.html, webrt/ViewUser.html:
+
+       more debug logging + some bugfixing (exception handling)
+       
+2000-07-10 20:09  jesse
+
+       * bin/testdeps.pl, etc/schema.mysql, lib/RT/Ticket.pm:
+
+       work on ACLs. some acl decisions are now made.
+       
+2000-07-10 17:13  jesse
+
+       * etc/schema.mysql, lib/RT/ACL.pm, lib/RT/CurrentUser.pm,
+       lib/RT/Queue.pm, lib/RT/Record.pm, lib/RT/Ticket.pm,
+       lib/RT/Transaction.pm, lib/RT/User.pm, lib/RT/Interface/Email.pm,
+       webrt/Ticket/Display.html:
+
+       Wow. all kinds of work.
+       
+       Beginning of implementation of ACLs. lots of code cleanup.
+       
+       schema changes for Groups and Acls.
+       
+       we now use MIMEObj EVERYWHERE instead of MIMEEntity.
+       
+2000-06-29 20:36  jesse
+
+       * Makefile:
+
+       bumped to 1.0.4pre2
+       fixed perms issue for lib/templates
+       
+2000-06-29 16:11  jesse
+
+       * Makefile, etc/schema:
+
+       makefile fixes. bump to 1.0.4pre1
+       schema.mysql has a longer phone number field
+       
+2000-06-26 15:35  jesse
+
+       * README:
+
+       [no log message]
+       
+2000-06-26 15:10  jesse
+
+       * Makefile, bin/rtmux.pl, bin/testdeps.pl:
+
+       added Date::Manip to testdeps.pl
+       set $ENV{'TZ'} in bin/rtmux.pl to deal with a bug in Date::Manip
+       bumped the version to 1.3.8
+       
+2000-06-26 14:38  jesse
+
+       * bin/testdeps.pl:
+
+       added Date::Kronos to testdeps.pl
+       
+2000-06-26 14:36  jesse
+
+       * bin/testdeps.pl:
+
+       added Time::Seconds to testdeps.pl
+       
+2000-06-26 14:31  jesse
+
+       * bin/testdeps.pl, webrt/autohandler, webrt/webrt.css,
+       webrt/Elements/ViewUser, webrt/Ticket/Elements/ShowTransaction:
+
+       various hacking for RTCon pilsen. a much enhanced testdeps.pl
+       
+2000-06-16 10:51  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       some commentary.
+       
+2000-06-16 08:57  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       Sort of a bugfix
+       
+2000-06-16 08:56  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       Removed some old garbage
+       
+       TODO: Fix message trailer
+       
+2000-06-16 04:37  tobiasb
+
+       * lib/RT/: Attachment.pm, Record.pm, Ticket.pm, Transaction.pm:
+
+       It's no longer mandatory to have a Creator in every RT table.  It's just to set them 'read/auto'-accessible.  This should fix bug #275
+       
+2000-06-15 16:43  tobiasb
+
+       * etc/config.pm, lib/RT/Attachment.pm, lib/RT/Link.pm,
+       lib/RT/Ticket.pm, webrt/Elements/MessageBox,
+       webrt/Ticket/Display.html, webrt/Ticket/LinkIt.html,
+       webrt/Ticket/ProcessUpdate.html, webrt/Ticket/Update.html,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       some few bugfixes + some work to get 'reply-linking' working.  In my local version of RT (only changes in the config.pm) people can now enter a 'FAQ-tag', and the right article from KB is automaticly inserted in the reply.
+       
+2000-06-15 12:22  tobiasb
+
+       * webrt/ViewUser.html:
+
+       file ViewUser.html was initially added on branch rt-1-1.
+       
+2000-06-15 12:22  tobiasb
+
+       * lib/RT/Attachment.pm, lib/RT/CurrentUser.pm, lib/RT/Ticket.pm,
+       lib/RT/User.pm, lib/RT/Interface/Email.pm, webrt/ViewUser.html,
+       webrt/autohandler, webrt/Elements/Error, webrt/Elements/Footer,
+       webrt/Elements/Header, webrt/Elements/ViewUser,
+       webrt/Ticket/ProcessUpdate.html, webrt/Ticket/Update.html,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       lots of bugfixes and some enhancements:
+       - signature in replies
+       - Creator, LastUpdated etc. should be updated in User.pm
+       - The message headers of inbound requests is now decoded ... this is
+         some sort of a hack as it discards charset information in the header.
+         Anyway, in those cases where the header data will be in the same charset
+         as the body, this works out nicely.
+       - I'm allowing a user to see what data is stored at him.  We might need ACLs
+         for the user comment.  Also, this one is placed directly on the root, so
+         it will break if the user is not logged in.  Also, the page and the module
+         is 'ViewUser', 'ShowUser' would be more consistent.
+       - Prettified the ShowTransaction header
+       
+2000-06-15 09:02  tobiasb
+
+       * etc/schema.mysql:
+
+       Added "Signature" to the Users table and worked a bit at the comments
+       
+2000-06-14 09:24  tobiasb
+
+       * lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       webrt/Ticket/Display.html, webrt/Ticket/Elements/TicketToolBox:
+
+       Tons of minor bugfixes and enhancements
+       
+2000-06-14 09:19  tobiasb
+
+       * etc/schema.mysql:
+
+       Added this:
+       
+       ##TODO: Get Notify.pm to support OldOwner + fix a template
+       #INSERT INTO Scrips VALUES (20, 'NotifyOldOwnerOnSteal',
+       #                         'Sends mail to the old owner when the ticket is stolen',
+       #                         'Steal','Notify',10,'OldOwner',1,NULL,1,NULL);
+       
+2000-06-14 06:54  tobiasb
+
+       * etc/schema.mysql, lib/RT/Attachment.pm, lib/RT/Record.pm,
+       lib/RT/Ticket.pm, webrt/Ticket/Display.html,
+       webrt/Ticket/Elements/ShowDates:
+
+       minor enhancements and bugfixes
+       
+2000-06-14 03:30  tobiasb
+
+       * etc/schema.mysql:
+
+       I just noticed that Jesse already had a 'resolution' attribute in the ticket.  I guess that covers the same as my 'state' attribute
+       
+2000-06-14 00:12  tobiasb
+
+       * lib/RT/Attachment.pm, webrt/Elements/ViewUser,
+       webrt/Search/Listing.html, webrt/Search/PickRestriction,
+       webrt/Ticket/Elements/ShowDates:
+
+       Some enhancements and bugfixes
+       
+2000-06-13 23:26  tobiasb
+
+       * webrt/Ticket/Elements/ShowDates:
+
+       improved
+       
+2000-06-13 20:42  tobiasb
+
+       * etc/schema.mysql:
+
+       Bugfix
+       
+2000-06-13 15:53  tobiasb
+
+       * lib/RT/TicketCollection.pm, webrt/Search/Listing.html:
+
+       Trying to get search on requestor to work - unsuccessful so far :/
+       
+2000-06-13 10:23  tobiasb
+
+       * webrt/: autohandler, Ticket/Display.html,
+       Ticket/Elements/TicketToolBox:
+
+       Some bugfixes
+       
+2000-06-13 06:41  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Bugfix
+       
+2000-06-09 14:43  tobiasb
+
+       * webrt/EditUserComments.html:
+
+       Hm, aaaalways check if everything is working before committing.  Argh.
+       
+2000-06-09 14:30  tobiasb
+
+       * webrt/: EditUserComments.html, autohandler, Elements/Error,
+       Elements/ViewUser:
+
+       added a minimum interface for editing comments about users
+       
+2000-06-09 12:53  tobiasb
+
+       * webrt/Ticket/Elements/ShowRequestor:
+
+       file ShowRequestor was initially added on branch rt-1-1.
+       
+2000-06-09 12:53  tobiasb
+
+       * webrt/Elements/ViewUser:
+
+       file ViewUser was initially added on branch rt-1-1.
+       
+2000-06-09 12:53  tobiasb
+
+       * webrt/EditUserComments.html:
+
+       file EditUserComments.html was initially added on branch rt-1-1.
+       
+2000-06-09 12:53  tobiasb
+
+       * etc/schema.mysql, lib/RT/Ticket.pm, lib/RT/User.pm,
+       lib/RT/Users.pm, lib/RT/Watcher.pm, lib/RT/Interface/Email.pm,
+       webrt/EditUserComments.html, webrt/Elements/ViewUser,
+       webrt/Ticket/ProcessUpdate.html, webrt/Ticket/Elements/ShowPeople,
+       webrt/Ticket/Elements/ShowRequestor,
+       webrt/Ticket/Elements/ShowSummary:
+
+       added a large section in the ticket view for earlier requests (stubbed) by the same requestor and comments about the requestor (the interface for editing those comments are stubbed)
+       
+2000-06-09 04:32  tobiasb
+
+       * etc/schema.mysql:
+
+       Added "Lang" to the users table
+       
+2000-06-08 10:17  tobiasb
+
+       * etc/config.pm, etc/schema.mysql, lib/RT/Ticket.pm,
+       webrt/Elements/SelectMatch, webrt/Ticket/Display.html,
+       webrt/Ticket/Elements/TicketToolBox:
+
+       Added 'UpdateTold' sub to Tickets.  Some minor bugfixes and enhancements.  Not much testing done.
+       
+2000-06-08 08:19  tobiasb
+
+       * lib/RT/Ticket.pm, lib/RT/Transaction.pm, lib/RT/Watcher.pm,
+       lib/RT/Action/SendEmail.pm, lib/RT/Interface/Email.pm,
+       webrt/Ticket/Update.html:
+
+       A bit of debugging, and I managed to remove some annoying warnings
+       
+2000-06-08 08:10  tobiasb
+
+       * etc/schema.mysql:
+
+       bugfix
+       
+2000-06-08 07:59  tobiasb
+
+       * etc/schema.mysql:
+
+       Removed the area in one template.
+       
+2000-06-08 07:55  tobiasb
+
+       * etc/schema.mysql:
+
+       # I think it might make sense replacing "TIMESTAMP" with "DATETIME"
+       # for mysql.  Yes, indeed, I'll do that right away.
+       
+2000-06-08 06:53  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Removed some redundant lines.
+       
+2000-06-08 06:38  tobiasb
+
+       * etc/schema.mysql:
+
+       Inserted table AlternateEmails (commented out as for now).
+       
+       One bugfix (typo) in the ACL table.
+       
+2000-06-08 06:31  tobiasb
+
+       * etc/schema.mysql:
+
+       Grouped into folders
+       
+2000-06-08 05:47  tobiasb
+
+       * lib/RT/Attachment.pm, lib/RT/Ticket.pm, webrt/Login.html,
+       webrt/Search/Listing.html, webrt/Ticket/ProcessUpdate.html,
+       webrt/Ticket/Elements/ShowTransaction,
+       webrt/Ticket/Elements/TicketToolBox:
+
+       bugfixes + improvements requested from the support team; kill & take link from the front page, table borders in listing, reduced header printing ...
+       
+2000-06-07 16:15  tobiasb
+
+       * lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       webrt/Elements/MessageBox, webrt/Ticket/Elements/ShowTransaction:
+
+       bugfixes
+       
+2000-06-07 15:46  tobiasb
+
+       * etc/schema.mysql:
+
+       bugfix
+       
+2000-06-07 15:42  tobiasb
+
+       * lib/RT/: Ticket.pm, Transaction.pm:
+
+       bugfixes
+       
+2000-06-07 15:22  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Now, if some Record decendant can _Accessible('Created', auto) and _Accessible('LastUpdated', auto), they will be set automaticly (at least, that's the theory)
+       
+2000-06-07 14:16  tobiasb
+
+       * etc/schema.mysql:
+
+       mysql autosets the first TIMESTAMP in the table on every
+       update/insert.
+       
+2000-06-07 12:33  tobiasb
+
+       * webrt/autohandler:
+
+       The error handling here seems to work now ... but only for documents
+       at the root.  I thought those autohandlers were called recursively?
+       
+2000-06-07 12:12  tobiasb
+
+       * etc/config.pm, lib/RT/Record.pm, lib/RT/Ticket.pm:
+
+       working with those pesky dates
+       
+2000-06-07 11:51  tobiasb
+
+       * bin/webmux.pl:
+
+       bugfix
+       
+2000-06-07 08:37  tobiasb
+
+       * README:
+
+       Added Date::Kronos (not on CPAN yet) to README
+       
+2000-06-07 08:17  tobiasb
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       Different Comment and Reply links.  They link to the same page, but the default
+       action is changed.
+       
+2000-06-07 07:36  tobiasb
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       Now it doesn't display looooong text/plain attachments.
+       
+       We have a lot of ~1M text/plain attachments in our RT queue.
+       
+       I'm trying to hack my webrt.cgi to show attachments upon a
+       "PickUpTransaction" parameter.  I don't know how to do it in the
+       mason/mod_perl model.
+       
+2000-06-07 04:09  tobiasb
+
+       * webrt/Elements/MessageBox:
+
+       bugfix
+       
+2000-06-06 18:44  jesse
+
+       * etc/schema.mysql:
+
+       removed an outdated table
+       
+2000-06-06 15:42  jesse
+
+       * webrt/Login.html:
+
+       changed the name of the mason package to RT::Mason, so it didn't conflict with other instances
+       on the same webserver
+       
+       fixed a couple of Log::Dispatch(warn)s to (warning)s
+       
+       removed tobix' weird session crunching code that was messign me up
+       
+2000-06-06 15:42  jesse
+
+       * etc/config.pm, lib/RT/Ticket.pm:
+
+       changed the name of the mason package to RT::Mason, so it didn't conflict with other instances
+       on the same webserver
+       
+       fixed a couple of Log::Dispatch(warn)s to (warning)s
+       
+2000-06-06 15:41  jesse
+
+       * bin/webmux.pl:
+
+       changed the name of the mason package to RT::Mason, so it didn't conflict with other instances
+       on the same webserver
+       
+       f
+       
+2000-06-06 15:41  jesse
+
+       * README:
+
+       changed the name of the mason package to RT::Mason, so it didn't conflict with other instances
+       on the same webserver
+       
+2000-06-06 14:40  tobiasb
+
+       * webrt/Ticket/: Update.html, Elements/TicketToolBox:
+
+       I'm trying to set default action in Update.html dependent on which link the
+       user clicked at (update, comment or reply).  I'll continue tomorrow (dinnertime
+       now :)
+       
+2000-06-06 14:11  tobiasb
+
+       * webrt/Ticket/ProcessUpdate.html:
+
+       bugfix
+       
+2000-06-06 13:48  jesse
+
+       * etc/config.pm:
+
+       changed Log::Dispatch::VERSION (1.2) which didn't work
+       to the more standard perl syntax:  use Log::Dispatch 1.2;
+       
+       \ed an @ that tobix left in rtmux.pl
+       
+2000-06-06 13:47  jesse
+
+       * bin/rtmux.pl:
+
+       \ed an @ that tobix left in rtmux.pl
+       
+2000-06-06 13:32  jesse
+
+       * Makefile:
+
+       :%s/        /   / in the makefile (convert spaces to tabs)
+       
+2000-06-06 13:15  tobiasb
+
+       * README:
+
+       Mostly doc changes
+       
+2000-06-06 11:36  tobiasb
+
+       * etc/config.pm:
+
+       bugfix
+       
+2000-06-06 11:15  tobiasb
+
+       * docs/FAQ:
+
+       Added some few tips
+       
+2000-06-06 11:05  tobiasb
+
+       * bin/rtmux.pl:
+
+       putted in some warnings about things that doesn't work
+       
+2000-06-06 04:21  tobiasb
+
+       * README:
+
+       added comments about DBMS'es
+       
+2000-06-05 16:54  tobiasb
+
+       * Makefile:
+
+       more bugfixes
+       
+2000-06-05 16:46  tobiasb
+
+       * Makefile:
+
+       bgfx
+       
+2000-06-05 14:01  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+2000-06-05 09:26  tobiasb
+
+       * README, etc/config.pm, etc/schema.mysql, webrt/autohandler:
+
+       done some documentation fixes
+       
+2000-06-05 07:14  tobiasb
+
+       * README:
+
+       updated the STATUS
+       
+2000-06-02 03:32  tobiasb
+
+       * etc/schema.mysql, lib/RT/Action/SendEmail.pm,
+       lib/RT/Interface/Email.pm, webrt/autohandler, webrt/Elements/Error:
+
+       bugfix
+       
+2000-06-01 23:28  tobiasb
+
+       * webrt/Ticket/Elements/TicketToolBox:
+
+       file TicketToolBox was initially added on branch rt-1-1.
+       
+2000-06-01 23:28  tobiasb
+
+       * etc/config.pm, etc/schema.mysql, lib/RT/Link.pm,
+       lib/RT/Ticket.pm, lib/RT/Action/SendEmail.pm,
+       lib/RT/Interface/Email.pm, webrt/autohandler, webrt/Elements/Error,
+       webrt/Elements/SelectOwner, webrt/Ticket/Create_Detail.html,
+       webrt/Ticket/Display.html, webrt/Ticket/ProcessUpdate.html,
+       webrt/Ticket/Update.html, webrt/Ticket/Elements/TicketToolBox:
+
+        ... better logging (logs a warning when it's dieing during an eval) ... maybe the URL in the mails will work now ... misc bugfixes ... some better foldings ... better error handling in the WebUI (not tested!  You should absolutely see this, I have a feeling it's unsmart.  Check autohandler and Elements/Error) ... misc features added (default values on selectowners now work, auto-fill-in of requestor on create, auto-fill-in of subject on spawn, viewing actions on the ProcessUpdate, ProcessUpdate fixed up a bit ... hm, that's it)
+       
+2000-06-01 20:54  tobiasb
+
+       * bin/testdeps.pl:
+
+       Added Text::Wrapper for quoting of wide messages
+       
+2000-06-01 20:50  tobiasb
+
+       * lib/RT/Transaction.pm, lib/RT/Action/StallDependent.pm,
+       lib/RT/Interface/Email.pm, webrt/Login.html,
+       webrt/Ticket/autohandler, webrt/Ticket/Elements/ShowLinks:
+
+       lots of bugfixes and minor enhancements
+       
+2000-06-01 02:58  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+2000-06-01 02:20  jesse
+
+       * Makefile, NEWS:
+
+       work on the web ui. cleanups to the comment and reply pages as well as the
+       queue view and the ticket detail page
+       
+2000-05-31 14:28  tobiasb
+
+       * etc/config.pm, webrt/Search/Listing.html,
+       webrt/Search/TicketCell, webrt/Ticket/Display.html:
+
+       Popular demand: A link for taking an action from the listing page (reduces the change for concurrency problems)
+       
+2000-05-31 13:59  tobiasb
+
+       * webrt/Search/autohandler:
+
+       Uh, some weird debugging stuff came into the last commit - I should
+       have checked better.
+       
+       Some of those autohandlers seems like copies of each other.  There
+       must be better ways to do this (i.e. separating all the common stuff
+       into some module)?
+       
+2000-05-31 13:52  tobiasb
+
+       * lib/RT/Transaction.pm, lib/RT/Interface/Email.pm,
+       webrt/Search/Listing.html, webrt/Search/PickRestriction,
+       webrt/Search/autohandler, webrt/Ticket/Display.html,
+       webrt/Ticket/Elements/ShowHistory:
+
+       popular demands; more navigation ({back to listing} and {last transaction}) and misc details
+       
+2000-05-31 12:20  tobiasb
+
+       * webrt/Elements/SelectMatch:
+
+       This one is plain ugly at the moment.  Anyway, it's ment for a more advanced 'regexp match / glob match / word match / substring match / total match'
+       
+2000-05-31 12:20  tobiasb
+
+       * webrt/Elements/SelectMatch:
+
+       file SelectMatch was initially added on branch rt-1-1.
+       
+2000-05-31 11:55  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       bugfix
+       
+2000-05-31 09:53  tobiasb
+
+       * webrt/: Elements/SelectStatus, Ticket/Update.html:
+
+       Some popular demands from the support dept;
+       
+       I've made it possible to kill by selecting status dead
+       
+       Response is the default action.  I've added a cryptical line which
+       might or might not give a warning.  Ideally, I guess we should do the comment /
+       reply selection through submit buttons rather than through a select menu - what
+       di you think?
+       
+2000-05-30 22:46  tobiasb
+
+       * etc/config.pm, lib/RT/Attachment.pm:
+
+       Ah ... I had forgotten signatures.  Some stubbed work, I'll continue tomorrow
+       
+2000-05-30 22:31  tobiasb
+
+       * lib/RT/Attachment.pm, lib/RT/Attachments.pm, lib/RT/Record.pm,
+       lib/RT/Ticket.pm, lib/RT/Transaction.pm, webrt/Login.html,
+       webrt/Elements/MessageBox, webrt/Search/Listing.html,
+       webrt/Ticket/autohandler, webrt/Ticket/Elements/ShowTransaction:
+
+       Quoting works now ... fixed some other misc details + bugfixes ... and I've moved the date handling to RT::Record, it will be completed tomorrow (I hopecvs diff -uw | less)
+       
+2000-05-30 20:31  tobiasb
+
+       * webrt/Ticket/Create_Detail.html:
+
+       Removed those annoying blink tags.. :)
+       
+2000-05-30 20:14  tobiasb
+
+       * webrt/: Elements/MessageBox, Ticket/Create_Detail.html:
+
+       Still working on that MessageBox
+       
+2000-05-30 20:09  tobiasb
+
+       * webrt/: Elements/MessageBox, Ticket/Update.html:
+
+       Separated out the MessageBox
+       
+2000-05-30 20:09  tobiasb
+
+       * webrt/Elements/MessageBox:
+
+       file MessageBox was initially added on branch rt-1-1.
+       
+2000-05-30 19:56  tobiasb
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       Changed <BLOCKQUOTE> to <pre> for the message content.
+       
+       TODO: We really should HTML'ify and/or HTML-escape the Content.
+       
+2000-05-29 06:59  tobiasb
+
+       * Makefile:
+
+       Some bugfixes, but absolutely not tested.
+       
+2000-05-26 12:11  tobiasb
+
+       * webrt/Search/Listing.html:
+
+       Now it runs /Elements/Header - I was a bit annoyed because it was missing Title
+       
+2000-05-26 10:11  tobiasb
+
+       * webrt/Ticket/Display.html:
+
+       Seems like I had introduced an error here
+       
+2000-05-24 21:50  jesse
+
+       * lib/RT/Ticket.pm, lib/RT/TicketCollection.pm, lib/RT/Tickets.pm,
+       webrt/Search/Listing.html, webrt/Ticket/Display.html:
+
+       Working on persistable web queries. Introduced RT::TicketCollection
+       Arguably this should be in RT::Tickets. and may be some day
+       
+2000-05-24 21:50  jesse
+
+       * lib/RT/TicketCollection.pm:
+
+       file TicketCollection.pm was initially added on branch rt-1-1.
+       
+2000-05-24 08:00  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       It should also be possible to "unlink" something (i.e. if a link was
+       wrongly set).  This is not implemented, but at least mails with
+       an "unlink" command will not bounce now.
+       
+2000-05-23 18:49  tobiasb
+
+       * lib/RT/Transaction.pm:
+
+       Made the transaction text from a link action a bit more readable,
+       though this still needs quite some work.
+       
+2000-05-23 16:57  jesse
+
+       * Makefile:
+
+       Bumped to 1.3.7
+       
+2000-05-23 10:12  tobiasb
+
+       * etc/config.pm, lib/RT/Link.pm, lib/RT/Ticket.pm,
+       lib/RT/Interface/Email.pm, webrt/Ticket/Display.html:
+
+       I've started looking at the mail interface.  Now linking works through the mailgate, though we need some info in the docs.  I'm proposing that it should accept both commands in the headerlines ("RT-Command: Link/Resolve/Whatever blahblah") and traditional commands in the mail ("%RT Link", "%RT Resolve", etc), though I've only supported the first one yet.  Look at the comments in Email.pm for details.
+       
+2000-05-23 10:04  tobiasb
+
+       * lib/RT/Scrip.pm:
+
+       Bugfix/optimalization or something?
+       
+2000-05-22 19:09  tobiasb
+
+       * lib/RT/Ticket.pm, webrt/Ticket/Create_Detail.html,
+       webrt/Ticket/Display.html, webrt/Ticket/LinkIt.html:
+
+       I'm not very happy about some comprimises, choises and short-cuts I've done here, but anyway linking & spawning seems to work now.
+       
+2000-05-22 18:05  tobiasb
+
+       * lib/RT/Transaction.pm, lib/RT/Action/AutoReply.pm,
+       webrt/Ticket/Create.html, webrt/Ticket/Create_Detail.html,
+       webrt/Ticket/Display.html, webrt/Ticket/LinkIt.html:
+
+       all changes here are either related to getting the Create working from the web, or harmless details
+       
+2000-05-22 17:39  tobiasb
+
+       * webrt/Ticket/Display.html:
+
+       $Subject => $Subject||(no subject given)
+       
+       TODO: HTML-escape
+       
+2000-05-22 17:38  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Removed VerboseSubject and HTMLSubject
+       
+2000-05-22 17:24  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       I do use $Ticket->Subject (and other stuff as well) frequently when
+       I'm editing Mason.  There is two issues;
+       
+       1) Things might look freaked if the ticket has no subject, i.e. Display.html
+       
+       2) Things might get totally whacko if the subject contains HTML code.
+       
+       I could ensure the "right" behaviour all the places by using
+       HTML::Entities::encode (I've done it one place), and by always using
+       $Subject||"(No subject)".  Anyway, I think it might be more
+       appropriate to have a sub RT::Ticket::VerboseSubject and
+       RT::Ticket::HTMLSubject.
+       
+       What do you think?
+       
+2000-05-22 16:51  tobiasb
+
+       * webrt/Ticket/Display.html:
+
+       It seems like it can create and view new transactions now.
+       
+       My next step will be to fix linking
+       
+2000-05-22 16:47  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       I should never commit before testing ... I should never commit before testing ... I should never commit before ...
+       
+       bugfix.
+       
+2000-05-22 16:38  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       I'm trying to get a better propagation of error messages.  I'm a bit
+       amused to find lots of things that shouldn't pass `perl -w' and `use
+       strict'?
+       
+2000-05-22 15:52  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Removed a debug (?) line that was commented out:
+       
+       #      print "From is $From\n";
+       
+       ...and inserted some other logging:
+       
+       - logs an info message with ticket id, subject and queue upon
+       successful message creation.
+       
+       - logs a warning if it couldn't be created.
+       
+2000-05-22 14:11  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       In RT::Ticket::Create: Defaulting queue to 'general', and warning when it's not set
+       
+2000-05-22 14:06  tobiasb
+
+       * README:
+
+       Added a 'rt-comment' to the recommended mail configuration
+       
+2000-05-22 14:04  tobiasb
+
+       * Makefile:
+
+       added a comment-mail-alias
+       
+2000-05-22 14:03  tobiasb
+
+       * etc/config.pm:
+
+       Hm ... $RT::CorrespondAddress is used in AutoReply.pm, while only
+       $MailAlias is set in config.pm.  I'm not sure if this is the Right(tm)
+       solution, but anyway ... I added those two lines, additions to the
+       Makefile and README file is coming up shortly ...
+       
+       $CorrespondAddress=$MailAlias;
+       $CommentAddress="!!RT_COMMENT_MAIL_ALIAS!!";
+       
+2000-05-22 12:53  tobiasb
+
+       * webrt/Ticket/Create_Detail.html:
+
+       fixed up a bug caused by too quick usage of cut&paste
+       
+2000-05-22 12:01  tobiasb
+
+       * webrt/Ticket/Create_Detail.html:
+
+       Done some work making this page look OK.  Now I will put in some logic
+       in Display.html for creating and linking requests.
+       
+2000-05-22 05:30  tobiasb
+
+       * webrt/Elements/SelectOwner:
+
+       # TODO: respect the ACLs!!
+       # This will be a list of some thousands requestors with the current scheme.
+       
+2000-05-19 12:37  tobiasb
+
+       * webrt/Ticket/Create.html:
+
+       Added a box for setting link information.  Only a web mockup, the
+       logic doesn't work yet.  Feel free to do whatever you'd like with this
+       one.  It might make sense to move this to the details page.
+       
+2000-05-19 12:17  jesse
+
+       * webrt/Ticket/LinkIt.html:
+
+       Code and formatting cleanups
+       
+2000-05-19 11:23  tobiasb
+
+       * etc/config.pm:
+
+       Removed some weird stuff
+       
+2000-05-19 10:59  jesse
+
+       * webrt/: index.html, Search/PickRestriction:
+
+       fixed some typos
+       
+2000-05-19 10:59  jesse
+
+       * etc/config.pm:
+
+       cleaned up the config file a bit
+       
+       <M-}>CVS:       etc/config.pm
+       
+2000-05-19 08:42  tobiasb
+
+       * bin/testdeps.pl, bin/webmux.pl, webrt/Elements/SelectLinkType,
+       webrt/Ticket/Display.html, webrt/Ticket/LinkIt.html,
+       webrt/Ticket/ProcessUpdate.html, webrt/Ticket/Elements/ShowSummary,
+       webrt/Ticket/Elements/ShowTransaction:
+
+       Started the work getting links to work.  While the linking doesn't work yet, this is a hint about how I think things might work
+       
+2000-05-19 08:42  tobiasb
+
+       * webrt/Elements/SelectLinkType:
+
+       file SelectLinkType was initially added on branch rt-1-1.
+       
+2000-05-19 08:42  tobiasb
+
+       * webrt/Ticket/LinkIt.html:
+
+       file LinkIt.html was initially added on branch rt-1-1.
+       
+2000-05-18 14:46  jesse
+
+       * lib/RT/Ticket.pm:
+
+       a tiny bit of doc added to Ticket.pm
+       still not happy with the format
+       
+2000-05-18 10:56  tobiasb
+
+       * webrt/Ticket/autohandler:
+
+       undo of last commit; only local modifications for debugging
+       
+2000-05-18 10:51  tobiasb
+
+       * webrt/: Search/Listing.html, Ticket/Elements/ShowTransaction:
+
+       the previous commit had wrong loginfo; those files are modified according to the latest changes in etc/config.pm
+       
+2000-05-18 10:42  jesse
+
+       * webrt/Elements/SelectResultsPerPage:
+
+       adding missing file
+       
+2000-05-18 10:41  tobiasb
+
+       * webrt/: index.html, Search/Listing.html, Ticket/autohandler,
+       Ticket/Elements/ShowTransaction:
+
+       A front page for navigation.  This one needs work; Masonifying, generalizing and possibilities for customizations needs to be implemented
+       
+2000-05-18 10:41  tobiasb
+
+       * webrt/index.html:
+
+       file index.html was initially added on branch rt-1-1.
+       
+2000-05-18 10:36  tobiasb
+
+       * webrt/Elements/SelectResultsPerPage:
+
+       Added an empty file as for now
+       
+2000-05-18 10:36  tobiasb
+
+       * webrt/Elements/SelectResultsPerPage:
+
+       file SelectResultsPerPage was initially added on branch rt-1-1.
+       
+2000-05-18 05:48  tobiasb
+
+       * etc/config.pm:
+
+       I'm planning to deal with web customizations with one hash RT::WebOptions in config.pm.  This hash might contain callbacks and maybe names of extra Mason modules to pull in.  What do you think?
+       
+2000-05-17 19:08  jesse
+
+       * webrt/Search/autohandler:
+
+       file autohandler was initially added on branch rt-1-1.
+       
+2000-05-17 19:08  jesse
+
+       * webrt/Search/: Listing.html, PickRestriction, autohandler:
+
+       Work on searches. in the cli  -maxitems <n> now does the right thing
+       
+2000-05-16 17:48  jesse
+
+       * webrt/Ticket/Elements/ShowLinks:
+
+       file ShowLinks was initially added on branch rt-1-1.
+       
+2000-05-16 17:48  jesse
+
+       * bin/rtmux.pl, bin/webmux.pl, lib/RT/Link.pm, lib/RT/Ticket.pm,
+       lib/RT/Action/OpenDependent.pm, lib/RT/Action/ResolveMembers.pm,
+       lib/RT/Action/Spam.pm, lib/RT/Action/StallDependent.pm,
+       webrt/Elements/Error, webrt/Ticket/Elements/ShowLinks,
+       webrt/Ticket/Elements/ShowSummary:
+
+       Converted the new link code to a fully oo setup, rahter than a mishmash of
+       procedural and oo.  seperated out webrt/Ticket/Elements/ShowLinks
+       
+       It's by no means done, but it does seem to work.
+       
+       jesse
+       
+2000-05-16 12:30  tobiasb
+
+       * Makefile, etc/config.pm, lib/RT/Link.pm, lib/RT/Ticket.pm,
+       webrt/Ticket/Elements/ShowSummary:
+
+       Done quite some work at the right hand 'Other Links' window (well, it's not good enough, and it also lists dependencies as for now).  Jesse, you might want to do something with ShowSummary, my Mason code seems horribly ugly.
+       
+2000-05-16 09:18  tobiasb
+
+       * etc/config.pm:
+
+       (...)
+       # A hash table of convertion subs to be used for transforming RT Link
+       # URIs to URLs in the web interface.  If you want to use RT towards
+       # locally installed databases, this is the right place to configure it.
+       # (TODO!)
+       my %URI2HTML=
+           (
+            'fsck.com-rt' => sub {warn "stub!";},
+            'mozilla.com-bugzilla' => sub {warn "stub!";},
+            'fsck.com-kb' => sub {warn "stub!"}
+            );
+       (...)
+       
+       I will also make a sub RT::Links::URI2HTML which gives smart links to
+       internal references, and passes other URIs to this hash table, and
+       eventually we should make default subs for KB and RT.
+       
+       What do you think of this idea?
+       
+2000-05-16 04:58  tobiasb
+
+       * lib/RT/: Link.pm, Ticket.pm:
+
+       Enabled link display in the CLI
+       
+2000-05-16 04:43  tobiasb
+
+       * lib/RT/Transaction.pm:
+
+       Seems like I had broken something...(bugfix)
+       
+2000-05-13 19:56  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Right, we need to work a bit with the links API.
+       
+       - we need an AllLinks sub.  How to tell EasySearch to give both links
+       where we're BASE and were we're TARGET?
+       
+       - we need an "unresolved dependencies" according to the current
+       design.  How to tell if a dependency is unresolved?  Dependencies can
+       point out of this RT instance!
+       
+       - we might need more subs, but not right now anyway.
+       
+2000-05-12 11:32  tobiasb
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       partly a bugfix; 'QouteTransaction=attachment-id' doesn't work very well for transactions without attachment.  Maybe that link should only be for attachments, and not for transactions.  Anyway, I changed it to transaction-id instead.  And I also added 'spawn' to the title; I think 'spawning' should be done through the same page.  'spawn' is when creating a new request that is linked up somehow to the old one.  Maybe it rather should be titled 'forward' or 'create new' or something?
+       
+2000-05-12 07:54  tobiasb
+
+       * webrt/: Elements/Error, Ticket/Display.html:
+
+       Bugfixing ... ehrm, why don't I test things before committing? :)
+       
+2000-05-12 07:34  tobiasb
+
+       * webrt/Elements/Error:
+
+       It logs errors now
+       
+2000-05-12 07:32  tobiasb
+
+       * webrt/Ticket/Display.html:
+
+       dies if element can't be loaded
+       
+2000-05-12 06:55  tobiasb
+
+       * webrt/Login.html:
+
+       seems like CGI won't take a combined get+post
+       
+2000-05-12 06:51  tobiasb
+
+       * etc/config.pm:
+
+       bugfix
+       
+2000-05-11 12:45  jesse
+
+       * webrt/Elements/ShadedBox:
+
+       file ShadedBox was initially added on branch rt-1-1.
+       
+2000-05-11 12:45  jesse
+
+       * webrt/Elements/ShadedBox:
+
+       missed this one before. it's somewhat misnamed right now. basically, it
+       prints a title and content.
+       
+2000-05-11 12:39  jesse
+
+       * webrt/Elements/: TitleBoxEnd, TitleBoxStart:
+
+       the title box for webui
+       
+2000-05-11 12:39  jesse
+
+       * webrt/Elements/TitleBoxStart:
+
+       file TitleBoxStart was initially added on branch rt-1-1.
+       
+2000-05-11 12:39  jesse
+
+       * webrt/Elements/TitleBoxEnd:
+
+       file TitleBoxEnd was initially added on branch rt-1-1.
+       
+2000-05-11 12:37  jesse
+
+       * webrt/Ticket/: Display.html, Elements/ShowSummary:
+
+       hrml cleanup. it's not as pretty, but man does it render faster
+       
+2000-05-11 01:07  jesse
+
+       * webrt/: Elements/Footer, Elements/Header, Ticket/Display.html,
+       Ticket/Elements/ShowHistory, Ticket/Elements/ShowSummary,
+       Ticket/Elements/ShowTransaction:
+
+       
+       Cleanup
+       \
+       
+2000-05-11 00:46  tobiasb
+
+       * TODO:
+
+       Added a list
+       
+2000-05-10 23:43  jesse
+
+       * webrt/webrt.css:
+
+       file webrt.css was initially added on branch rt-1-1.
+       
+2000-05-10 23:43  jesse
+
+       * webrt/webrt.css:
+
+       The base stylesheet.
+       
+2000-05-10 23:41  jesse
+
+       * webrt/Ticket/Elements/ShowSummary:
+
+       file ShowSummary was initially added on branch rt-1-1.
+       
+2000-05-10 23:41  jesse
+
+       * webrt/Ticket/Elements/ShowTransaction:
+
+       file ShowTransaction was initially added on branch rt-1-1.
+       
+2000-05-10 23:41  jesse
+
+       * webrt/Ticket/Elements/ShowDates:
+
+       file ShowDates was initially added on branch rt-1-1.
+       
+2000-05-10 23:41  jesse
+
+       * webrt/Ticket/Elements/ShowHistory:
+
+       file ShowHistory was initially added on branch rt-1-1.
+       
+2000-05-10 23:41  jesse
+
+       * webrt/Ticket/Elements/ToolBar:
+
+       file ToolBar was initially added on branch rt-1-1.
+       
+2000-05-10 23:41  jesse
+
+       * webrt/Ticket/Elements/ShowBasics:
+
+       file ShowBasics was initially added on branch rt-1-1.
+       
+2000-05-10 23:41  jesse
+
+       * webrt/Ticket/Elements/ShowPeople:
+
+       file ShowPeople was initially added on branch rt-1-1.
+       
+2000-05-10 23:41  jesse
+
+       * webrt/: Elements/Header, Ticket/Display.html,
+       Ticket/DisplayHeader, Ticket/DisplaySummary, Ticket/DisplayTicket,
+       Ticket/ProcessUpdate.html, Ticket/ToolBar, Ticket/autohandler,
+       Ticket/Elements/ShowBasics, Ticket/Elements/ShowDates,
+       Ticket/Elements/ShowHistory, Ticket/Elements/ShowPeople,
+       Ticket/Elements/ShowSummary, Ticket/Elements/ShowTransaction,
+       Ticket/Elements/ToolBar:
+
+       Work on the web ui. mostly on the ticket display interface
+       
+2000-05-10 15:43  tobiasb
+
+       * README:
+
+       Added a comment about how to do the CGI :) Well:
+       
+               To get it up running even without mod_perl, you will need to
+       consult Tobix or the rt-devel mailinglist.  Tobix is actively working
+       on a (Fast)CGI version, but as for now he has been too lazy to update
+       the template for the cgi executable.
+       
+2000-05-10 15:39  tobiasb
+
+       * webrt/Ticket/Create_Detail.html:
+
+       added a require statement.  No effect for the mod_perl version, but makes sense for a CGI version.
+       
+2000-05-09 05:35  tobiasb
+
+       * bin/webmux.pl:
+
+       Better formatting
+       
+2000-05-08 05:01  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       bugfix
+       
+2000-05-05 19:20  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       This one shouldn't loop now - though it's not tested :)
+       
+2000-05-05 19:15  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       Fixing up the loop control.  When a possible loop occurs, an error
+       flag is set, and a new header field RT-Loop-Alarm appears.  I feel a
+       bit bad about the latter, we are indeed changing an email after it
+       arrived to the system, which I think is a bad thing - but anyway not
+       as bad as dropping the email completely.  My idea is that the Actions,
+       particularly the SendEmail.pm Action remain silent when this header
+       field is set.
+       
+2000-05-05 19:01  tobiasb
+
+       * etc/config.pm:
+
+       Added this about logging to the comments:
+       
+       # It might generally make sense to send error and higher by email to
+       # some administrator.  For heavens sake; be sure that the email goes
+       # directly to a mailbox, and not via RT :) Mail loops will generate a
+       # critical log message.
+       
+2000-05-05 16:47  jesse
+
+       * bin/testdeps.pl:
+
+       Added Log::Dispatch to testdeps.pl
+       
+2000-05-05 13:04  tobiasb
+
+       * lib/RT/Transaction.pm:
+
+       bugfix
+       
+2000-05-05 12:47  tobiasb
+
+       * etc/config.pm:
+
+       I need help, I can't really figure out from this;
+       
+       When commenting out the $SIG{__DIE__} stuff, everything works fine.
+       
+       When having this line in the config:
+       
+       $SIG{__DIE__}  = sub {$RT::Logger->log(level=>'crit',message=>$_[0]); print STDERR $_[0]; exit(-1);};
+       
+       I get this error:
+       
+       Can't locate Mail/Field/addrlist.pm in @INC (@INC contains: /tmp/FunRT /tmp/DBIx /etc/rt /usr/local/lib/perl5/5.6.0/i686-linux /usr/local/lib/perl5/5.6.0 /usr/local/lib/perl5/site_perl/5.6.0/i686-linux /usr/local/lib/perl5/site_perl/5.6.0 /usr/local/lib/perl5/site_perl .) at (eval 115)[/usr/local/lib/perl5/site_perl/5.6.0/Mail/Field.pm:87] line 3.
+       
+       WTF???
+       
+2000-05-05 10:29  tobiasb
+
+       * bin/rtmux.pl:
+
+       I guess this should block that stupid warnings
+       
+2000-05-05 10:01  tobiasb
+
+       * etc/config.pm:
+
+       Added Log::Dispatch.  I also have added those comments, which I would
+       like comments on (:
+       
+       # Most (if not all?) $RT:: global variables should be here.  I'd
+       # suggest putting session information in another Namespace (main:: or
+       # RT::main or maybe something like that).
+       
+       #use strict;
+       
+       #use vars qw/%SitePolicy $dirmode $transactionmode $DatabasePassword $rtname $domain $host $DatabaseHost $DatabaseUser $RT::DatabaseName $DatabaseType $user_passwd_min $MailAlias $WebrtImagePath $web_auth_mechanism $web_auth_cookies_allow_no_path $DefaultLocale $LocalePath $Nobody $Logger/;
+       
+       I'm not sure if "use strict" breaks, but at least "use vars" breaks
+       bigtime as we are using it other places (like rtmux.pl).  I suggest
+       using another package name for the execution logic.  What do you
+       think?
+       
+2000-05-05 08:07  tobiasb
+
+       * Makefile:
+
+       Added RT_LOGFILE, and comments on how to fix advanced logging
+       
+2000-05-05 02:31  jesse
+
+       * bin/rtmux.pl:
+
+       a bit of performance hacking. now require mason and cgi rather than 'use'ing them
+       
+2000-05-05 02:22  jesse
+
+       * lib/RT/: Action.pm, Scrip.pm, ScripScope.pm, Template.pm,
+       Ticket.pm, Transaction.pm, Action/AutoReply.pm,
+       Action/SendEmail.pm, Action/Spam.pm:
+
+       Cleaned up a couple minor things (like precedence, again) and changed "FixSubject" to "SetSubjectTag"
+       
+       Fixed the memory leak in Transaction.pm (it wasn't letting go of Scrip objects)
+       The fix was a bit of a "large mallet on a small nail" in that I'm now _explicitly_ destroying ScripObjects two different ways and don't quite understand why I have to do it. It does, however, work.
+       
+2000-05-04 09:00  tobiasb
+
+       * lib/RT/Action/Spam.pm:
+
+       Completely untested - but I guess this should do real spamming, that
+       is one email for each request(or).
+       
+2000-05-04 08:59  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       bugfix
+       
+2000-05-04 07:04  tobiasb
+
+       * lib/RT/Action/: AutoReply.pm, SendEmail.pm:
+
+       Precedence: Bulk should only be set for AutoReply
+       
+2000-05-04 05:29  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       sub FixSubject sets the RT tag (unless it is already there).  I moved
+       it out from Prepare because I need to override this behaviour when
+       Spamming people.
+       
+2000-05-04 01:34  jesse
+
+       * etc/schema.mysql:
+
+       spec some defaults in the schema
+       
+2000-05-04 01:32  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Bugfix to ticket.pm for better handling of default values for some queue fields
+       
+2000-05-03 14:19  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Sorry, the last commit on this file was not really a bugfix.  I'd say all Ticket (Transaction) Actions should return status, message and eventually optional things after the message.
+       
+2000-05-03 14:16  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Bugfix
+       
+2000-05-03 14:13  tobiasb
+
+       * lib/RT/Action/: ResolveMembers.pm, Spam.pm:
+
+       Those actually seems to work :)
+       
+2000-05-03 14:13  tobiasb
+
+       * lib/RT/Action/Spam.pm:
+
+       file Spam.pm was initially added on branch rt-1-1.
+       
+2000-05-03 14:13  tobiasb
+
+       * lib/RT/Action/ResolveMembers.pm:
+
+       file ResolveMembers.pm was initially added on branch rt-1-1.
+       
+2000-05-03 14:13  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       gubfix
+       
+2000-05-03 14:00  tobiasb
+
+       * etc/schema.mysql:
+
+       Bugfix
+       
+2000-05-03 13:29  tobiasb
+
+       * etc/schema.mysql:
+
+       Commented out the AutoReplies table.
+       
+2000-05-03 13:17  tobiasb
+
+       * lib/RT/Action/AutoReply.pm:
+
+       Putted in "#TODO: " comments for avoiding duplicate AutoReplies.
+       
+2000-05-03 13:15  tobiasb
+
+       * etc/schema.mysql:
+
+       Added a table AutoReplies to avoid sending the same autoreply template
+       more than once to each requestor.
+       
+       Eventually entries should be removed (i.e. after a week) through the
+       (upcoming?) timer system (eventually through the good, old crontab(5))
+       
+2000-05-03 11:48  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       LinkTo and LinkFrom are tested a bit, and they seem to work OK now.
+       
+2000-05-03 11:02  tobiasb
+
+       * etc/schema.mysql:
+
+       Bugfix.
+       
+2000-05-03 07:40  tobiasb
+
+       * etc/schema.mysql:
+
+       I'm putting ResolveGroupTicket on hold.  It's just not important.
+       
+2000-05-02 18:05  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Starting to add a bit of inline pod documentation. the style isn't finalized yet
+       
+2000-05-02 14:30  tobiasb
+
+       * lib/RT/Action/OpenDependent.pm:
+
+       file OpenDependent.pm was initially added on branch rt-1-1.
+       
+2000-05-02 14:30  tobiasb
+
+       * etc/schema.mysql, lib/RT/Link.pm, lib/RT/Ticket.pm,
+       lib/RT/Action/OpenDependent.pm, lib/RT/Action/StallDependent.pm:
+
+       I just got DependsOn Links to work as they should in the CLI :)
+       
+2000-05-02 13:36  tobiasb
+
+       * lib/RT/Links.pm:
+
+       file Links.pm was initially added on branch rt-1-1.
+       
+2000-05-02 13:36  tobiasb
+
+       * lib/RT/: Links.pm, Ticket.pm, Action/StallDependent.pm:
+
+       [no log message]
+       
+2000-05-02 13:07  tobiasb
+
+       * lib/RT/Action/StallDependent.pm:
+
+       Bugfix.  Also, only Open requests should be Stalled.
+       
+2000-05-02 13:01  tobiasb
+
+       * lib/RT/: Ticket.pm, Action/StallDependent.pm:
+
+       Bugfixing
+       
+2000-05-02 12:21  tobiasb
+
+       * lib/RT/: Ticket.pm, Action/StallDependent.pm:
+
+       bugfixes
+       
+2000-05-02 11:55  tobiasb
+
+       * etc/schema.mysql:
+
+       Added TobiX-style logic scrips for DependsOn link and MemberOf link.
+       
+2000-05-02 07:55  tobiasb
+
+       * lib/RT/: Link.pm, Ticket.pm:
+
+       oup, forgot to keep a consistant style
+       base => Base
+       target => Target
+       etc
+       
+2000-05-02 07:55  tobiasb
+
+       * Makefile, etc/config.pm:
+
+       forgot to commit those changes ... configuration for the URI method
+       
+2000-05-02 05:45  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Agh, compile error.
+       
+2000-05-02 05:30  tobiasb
+
+       * lib/RT/Action/StallDependent.pm:
+
+       file StallDependent.pm was initially added on branch rt-1-1.
+       
+2000-05-02 05:30  tobiasb
+
+       * lib/RT/Action/: README.hackers, StallDependent.pm:
+
+       Ok, so lets put all Actions into one directory - though I'd say it would be better for future hackers to orient themselves if we grouped them somehow.
+       
+2000-05-02 00:11  jesse
+
+       * Makefile:
+
+       Version bumping
+       
+2000-05-01 17:16  jesse
+
+       * lib/RT/Ticket.pm:
+
+       moved URIIsLocal closer to other linking routines.
+       
+2000-05-01 10:16  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Made the transaction data a bit "nicer", should look something like:
+       
+       THIS DependsOn 4323 as of 342
+       <URI> DependsOn THIS as of 343
+       
+2000-05-01 08:58  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Changed URL -> URI
+       
+       Changed NewLink and ReverseLink to LinkTo and LinkFrom
+       
+       Added one byte to indicate if it's a From-link or a To-link in the
+       transaction data.
+       
+2000-05-01 01:52  jesse
+
+       * etc/schema.mysql:
+
+       fixed a few typos in the schema
+       
+2000-05-01 01:50  jesse
+
+       * lib/RT/ACL.pm:
+
+       Initial sketches at RT::ACL.pm. there's no running code here yet.
+       
+2000-05-01 01:48  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Fixed Ticket->URL. changed queue->load to queue->Load
+       
+2000-05-01 01:04  jesse
+
+       * bin/rtmux.pl:
+
+       Fixed improper rt program selection code.
+       
+2000-05-01 00:14  jesse
+
+       * lib/RT/Queue.pm:
+
+       a stub for ACL work.
+       
+2000-04-30 13:28  tobiasb
+
+       * lib/RT/Link.pm:
+
+       file Link.pm was initially added on branch rt-1-1.
+       
+2000-04-30 13:28  tobiasb
+
+       * lib/RT/: Link.pm, Ticket.pm:
+
+       Link.pm
+       
+2000-04-30 13:13  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Added a static method URIIsLocal which checks if it's a local URI or not.
+       
+2000-04-28 14:20  jesse
+
+       * lib/RT/Transaction.pm:
+
+       Cleaned up some comments and formatting. and did a bit of reorganizing
+       of routines to more closely model other files. Oh. i finally have
+       emacs doing proper indenting per perlstyle. so we'll hopefully start
+       to get better about that :) Tobix has only been bugging me for a year
+       
+2000-04-28 09:05  tobiasb
+
+       * lib/RT/Action/README.hackers:
+
+       Just some documentation for future hackers :)
+       
+2000-04-28 09:05  tobiasb
+
+       * lib/RT/Action/README.hackers:
+
+       file README.hackers was initially added on branch rt-1-1.
+       
+2000-04-28 08:20  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Started the work of implementing links
+       
+2000-04-28 04:40  tobiasb
+
+       * etc/schema.mysql:
+
+       Didn't we once agree that we needed "state" (user-configurable logic)
+       and "status" (fixed open/stalled/resolved/dead)? :)
+       
+2000-04-28 01:17  jesse
+
+       * lib/RT/Ticket.pm:
+
+       didn't balance parens. grr
+       
+2000-04-28 00:54  jesse
+
+       * lib/RT/Ticket.pm:
+
+       a bunch of cleanup.. a bunch more folding mode braces.
+       a few minor (still not in any codepath) changes for the start of some
+       acls work
+       
+2000-04-28 00:51  jesse
+
+       * docs/design_docs/link-definitions.txt:
+
+       a few comments, additions and clarifications. nothing really major
+       
+2000-04-27 02:06  jesse
+
+       * docs/design_docs/acls:
+
+       more thoughts on ACLs. this time including some code snippits.
+       
+2000-04-27 02:01  jesse
+
+       * etc/schema.mysql:
+
+       added the schema for the new ACL system.
+       responded to tobix' comments about GECOS.
+       
+2000-04-26 14:58  tobiasb
+
+       * docs/design_docs/subscription-definitions.txt:
+
+       marginal updates to reflect that scrips/actions can be used for more things than subscriptions (the whole document might need a rename)
+       
+2000-04-26 14:50  tobiasb
+
+       * docs/design_docs/link-definitions.txt, etc/schema.mysql:
+
+       only insignificant comments - some made weeks ago
+       
+2000-04-26 12:59  jesse
+
+       * docs/design_docs/link-definitions.txt:
+
+       some comments on link definitions
+       my comments are marked with # as the first character of the line.
+       
+2000-04-26 02:27  tobiasb
+
+       * docs/design_docs/: link-definitions.txt,
+       subscription-definitions.txt:
+
+       updates
+       
+2000-04-26 00:14  jesse
+
+       * docs/design_docs/acls:
+
+       some more hacking on acls design
+       
+2000-04-25 17:58  jesse
+
+       * docs/design_docs/acls:
+
+       thoughts on acls
+       
+2000-04-24 00:17  jesse
+
+       * webrt/Ticket/Create_Detail.html:
+
+       file Create_Detail.html was initially added on branch rt-1-1.
+       
+2000-04-24 00:17  jesse
+
+       * webrt/Ticket/Create_Detail.html:
+
+       some work on create. nothing ready for consumption yet.
+       
+2000-04-24 00:16  jesse
+
+       * webrt/: Logout.html, Elements/Footer, Elements/Header,
+       Elements/SelectQueue, Ticket/Create.html, Ticket/Display.html,
+       Ticket/ProcessUpdate.html, Ticket/SetOwner, Ticket/SetStatus,
+       Ticket/Update.html, Ticket/autohandler:
+
+       Look ma, $session{'CurrentUser'} contains the current user object.
+       you can depend on this being set if the user's authenticated.
+       
+2000-04-24 00:16  jesse
+
+       * webrt/Logout.html:
+
+       file Logout.html was initially added on branch rt-1-1.
+       
+2000-04-24 00:14  jesse
+
+       * Makefile, bin/testdeps.pl, bin/webmux.pl:
+
+       Added Apache::Session support to the HTML::Mason version of things.
+       
+2000-04-23 19:45  jesse
+
+       * bin/testdeps.pl:
+
+       added a little script to test perl dependencies.
+       it should probably get integrated into the makefile
+       
+2000-04-23 19:45  jesse
+
+       * bin/testdeps.pl:
+
+       file testdeps.pl was initially added on branch rt-1-1.
+       
+2000-04-23 17:28  jesse
+
+       * Makefile, docs/design_docs/acls, etc/schema.mysql:
+
+       installation seems to work a bit better now
+       
+2000-04-20 02:18  jesse
+
+       * webrt/Login.html:
+
+       Removing cruft from login.html
+       
+2000-04-20 02:17  jesse
+
+       * webrt/Ticket/autohandler:
+
+       Look ma! this autohandler seems to make webauth (with $pass = $user ) work.
+       
+2000-04-20 02:09  jesse
+
+       * webrt/: autohandler, Ticket/autohandler:
+
+       file autohandler was initially added on branch rt-1-1.
+       
+2000-04-20 02:09  jesse
+
+       * webrt/Login.html:
+
+       file Login.html was initially added on branch rt-1-1.
+       
+2000-04-20 02:09  jesse
+
+       * Makefile, bin/webmux.pl, webrt/Login.html, webrt/autohandler,
+       webrt/Ticket/autohandler:
+
+       Doing work on cookie-based authentication for WebRT. The basic idea
+       is that the autohandler in each directory (unless the global handlers can be made to work) will check to see if the user is authenticated. If they are, then it keeps going happily. otherwise, it forces the user to login with the form in /Login.html, which should eventually be smart enough to put them back to where they want to go.
+       
+2000-04-18 02:43  jesse
+
+       * docs/design_docs/acls:
+
+       checking in design work on ACLs. a bit more work, such as notes
+       on order of evaluation and a rough implementation plan and then I'll
+       implement
+       
+2000-04-13 11:58  tobiasb
+
+       * etc/config.pm:
+
+       Introduced all columns we need locally, that is all columns as of RT
+       1.0, except due, area and priorities.  I guess somebody might want to
+       add those columns as well :)
+       
+2000-04-13 11:52  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       hrmwell, you did introduse a slight bug here.. :)
+       
+2000-04-13 11:26  tobiasb
+
+       * webrt/Search/: Listing.html, TicketCell:
+
+       Looks gruesome, but hey - it works\!
+       
+2000-04-13 11:26  tobiasb
+
+       * webrt/Search/TicketCell:
+
+       file TicketCell was initially added on branch rt-1-1.
+       
+2000-04-13 10:52  tobiasb
+
+       * etc/config.pm:
+
+       agh..
+       
+2000-04-13 10:50  tobiasb
+
+       * etc/config.pm:
+
+       Added the site configurable option "QueueListingCols".  It should also
+       be possible to override this one at least through the query, but it
+       might not be trivial.  (maybe you have better suggestions?)
+       
+2000-04-13 10:23  tobiasb
+
+       * webrt/Search/QueueItem:
+
+       I think I want to remove this one, I have other thoughts about how to
+       handle this - let's discuss it more when I've made some implementation :)
+       
+2000-04-13 10:05  tobiasb
+
+       * webrt/Search/Listing.html:
+
+       Added this:
+       
+       # TODO: This one should _not_ be here, rather somewhere else
+       # (suggestions?).  It might eventually read the cookies, user
+       # configuration information from the DB, queue configuration information
+       # from the DB, etc.  It should be object oriented.  But what object can
+       # it belong to, and how should it get access to all this data?
+       
+       sub _cfg {
+         my $key=shift;
+         return $ARGS{$key} || $RT::SitePolicy{$key};
+       }
+       
+2000-04-13 10:05  tobiasb
+
+       * etc/config.pm:
+
+       Added a hash for "tunable" configurations:
+       
+       # This is where RT's preferences are kept track of
+       
+       package RT;
+       
+       # Different "tunable" configuration options should be in this hash:
+       %SitePolicy=();
+       
+2000-04-13 02:30  jesse
+
+       * lib/RT/Ticket.pm:
+
+       Ticket::clean became Ticket::_CleanAddressesAsString
+       references to clean changed accordingly. i feel a bit dirty not
+       making this $self->_CleanAddressesAsString.
+       
+2000-04-12 08:53  tobiasb
+
+       * webrt/: Elements/SelectStatus, Ticket/ProcessUpdate.html,
+       Ticket/Update.html:
+
+       bugfixes and insignificant enhancements
+       
+2000-04-12 07:14  tobiasb
+
+       * webrt/Elements/yearMenu:
+
+       "Last year" is now (by default) available
+       
+2000-04-12 07:11  tobiasb
+
+       * webrt/Search/PickRestriction:
+
+       Changed the default to select one queue, not select "anything but" a queue.
+       
+2000-04-12 07:07  tobiasb
+
+       * webrt/Search/PickRestriction:
+
+       Added a stupid "under construction" message.
+       
+2000-04-12 07:00  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       Ehm .. the last commit message should be more descriptive;
+       
+       From the README:
+       
+               rt:     |"/path/to/rt/bin/rt-mailgate general correspond"
+                                                           |          |
+                                          <<queue-name>----/          |
+                                                                      |
+                       <<correspond or comment depending on whether   |
+                        the mail shoud be resent to the requestor>---/
+                        "action" here will make this address only
+                         parse actions in the message without
+                         recording the message as a transaction
+                         of its own"
+       
+       ...while the implementation required "correspond general"
+       
+2000-04-12 06:58  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       This was not in sync with the README
+       
+2000-04-12 06:50  tobiasb
+
+       * webrt/Search/PickRestriction:
+
+       Bugfix
+       
+2000-04-12 06:45  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Agh, should have tested before committing :)
+       
+2000-04-12 06:44  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Fixd SetOwner to allow stealing and to respect the new Nobody user
+       
+2000-04-12 05:19  tobiasb
+
+       * webrt/Elements/SelectQueue:
+
+       Now this one actually works
+       
+2000-04-11 07:30  tobiasb
+
+       * webrt/Elements/SelectQueue:
+
+       This won't work yet.
+       
+2000-04-11 07:17  tobiasb
+
+       * lib/RT/Transaction.pm:
+
+       Ehr...right.  Still fixing at the same bug; the IsInbound sub didn't work....well
+       
+2000-04-11 07:08  tobiasb
+
+       * lib/RT/: Ticket.pm, Transaction.pm:
+
+       argh .. I should learn to test the bugfixes before committing
+       
+2000-04-11 06:46  tobiasb
+
+       * lib/RT/Transaction.pm:
+
+       bugfix
+       
+2000-04-11 06:42  tobiasb
+
+       * lib/RT/Template.pm, lib/RT/Ticket.pm, webrt/Elements/SelectOwner,
+       webrt/Ticket/ProcessUpdate.html:
+
+       bugfixes
+       
+2000-04-11 05:47  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       bugfixing
+       
+2000-04-11 04:46  tobiasb
+
+       * lib/RT/Watchers.pm:
+
+       Bugfix
+       
+2000-04-11 04:41  tobiasb
+
+       * etc/config.pm, lib/RT/Ticket.pm:
+
+       I'm trying to fix this nobody user..
+       
+2000-04-11 04:36  tobiasb
+
+       * etc/schema.mysql:
+
+       I'm trying to fix this nobody user..
+       
+2000-04-11 04:29  tobiasb
+
+       * lib/RT/Queue.pm, webrt/Ticket/ProcessUpdate.html:
+
+       Bugfixing
+       
+2000-04-11 03:16  tobiasb
+
+       * webrt/Search/Listing.html:
+
+       Insignificant.  Oh, how should errors be trapped?
+       
+2000-04-11 00:40  jesse
+
+       * lib/RT/: Queue.pm, Ticket.pm, Watchers.pm:
+
+       all the watcher types now have AsString methods. this was done by dropping
+       an EmailsAsString method into RT::Watcher
+       
+2000-04-10 23:04  tobiasb
+
+       * webrt/Ticket/ProcessUpdate.html:
+
+       Added those use statements
+       
+       This page seems very broken in Netscape.
+       
+2000-04-10 23:00  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Darn, I thought I had tested this.  Well, now it should work!?
+       
+2000-04-10 22:54  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       ...some work is needed to get Watchers work OK.  I commented out my
+       try on it, so now it will ignore queue watchers (it simply ignored all
+       watchers as it was).
+       
+2000-04-10 14:29  tobiasb
+
+       * webrt/Ticket/: DisplayTicket, DisplayTransaction:
+
+       Update and Modify-links
+       
+2000-04-10 14:20  tobiasb
+
+       * webrt/Ticket/DisplayTransaction:
+
+       Added an "act" link and touched the comments
+       
+2000-04-10 14:11  tobiasb
+
+       * webrt/Ticket/DisplaySummary:
+
+       Reinserted the "Last Contact" and "Last Update" lines, but they're "cleaned" a bit.
+       
+       (btw, this seems like a crude copy from the cli, does the cli have similar problems?)
+       
+2000-04-10 13:51  tobiasb
+
+       * webrt/Ticket/DisplaySummary:
+
+       Hacking a bit on this
+       
+2000-04-10 13:48  tobiasb
+
+       * webrt/Ticket/: DisplayTransaction, Update.html:
+
+       Hacking a bit on this
+       
+2000-04-10 13:18  tobiasb
+
+       * webrt/Ticket/Modify.html:
+
+       'undo' (well, not really :)
+       
+2000-04-10 13:13  tobiasb
+
+       * webrt/Ticket/Update.html:
+
+       This 'fix' is maybe only temporary?
+       
+2000-04-10 13:08  tobiasb
+
+       * webrt/Ticket/Modify.html:
+
+       I'd daresay this one is superceded by Update.html?
+       
+2000-04-10 13:07  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       just added some comments
+       
+2000-04-10 12:52  tobiasb
+
+       * webrt/Ticket/DisplaySummary:
+
+       This 'fix' is maybe only temporary?
+       
+2000-04-10 10:35  tobiasb
+
+       * webrt/: Elements/SelectBoolean, Search/Listing.html:
+
+       insignificant changes
+       
+2000-04-10 06:53  tobiasb
+
+       * webrt/Elements/SelectOwner:
+
+       Insignificant fix (wouldn't work here without)
+       
+2000-04-10 05:38  tobiasb
+
+       * bin/rtmux.pl:
+
+       Might be a bugfix
+       
+2000-04-09 16:56  jesse
+
+       * lib/RT/Users.pm, webrt/Elements/SelectBoolean,
+       webrt/Elements/SelectOwner, webrt/Search/Listing.html,
+       webrt/Search/PickRestriction:
+
+       I can now get a listing of tickets whose owner is or isn't a given owner.
+       and it even lists them. Next, I'll probably make searches for
+       other attributes work.
+       the plan of attack is requestor, then probably ticket content (as a test
+       of whether this whole system works ;)
+       
+2000-04-07 20:41  jesse
+
+       * webrt/Search/Listing.html:
+
+       Added the first bit of code to support queue/owner/requestor/subject search.
+       This will make up the core of the  web search functionality of the first release of the webui. (not the 2.0 release, but the "tobix needs it now) release.
+       
+2000-04-07 14:53  tobiasb
+
+       * etc/schema.mysql, lib/RT/Ticket.pm, lib/RT/Watchers.pm,
+       lib/RT/Action/SendEmail.pm:
+
+       hm ... seems like I forgot to commit my last work
+       
+2000-04-07 03:55  tobiasb
+
+       * lib/RT/: Ticket.pm, Transaction.pm:
+
+       it almost works now
+       
+2000-04-06 17:40  tobiasb
+
+       * lib/RT/Transaction.pm:
+
+       development
+       
+2000-04-06 17:21  tobiasb
+
+       * lib/RT/: Ticket.pm, Transaction.pm, Action/Notify.pm,
+       Action/SendEmail.pm:
+
+       ouch, found a lot of fatal stubs
+       
+2000-04-06 16:54  tobiasb
+
+       * etc/schema.mysql, lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       lib/RT/Action/Notify.pm, lib/RT/Action/SendEmail.pm:
+
+       still fighting with the mail handling
+       
+2000-04-06 16:12  tobiasb
+
+       * etc/schema.mysql, lib/RT/Scrips.pm, lib/RT/Ticket.pm,
+       lib/RT/Transaction.pm, lib/RT/Action/Notify.pm:
+
+       New snapshot of development.  Note the change I've done in the Type recognizion
+       when selecting the right Scrips in Transaction::Create - it should be more
+       flexible this way (replaced "ne" with "!~").  Check the change to schema for
+       an example of how this is more flexible.
+       
+2000-04-06 15:36  tobiasb
+
+       * lib/RT/Action/Notify.pm:
+
+       Recipient finder
+       
+2000-04-06 15:35  tobiasb
+
+       * etc/schema.mysql, lib/RT/Template.pm, lib/RT/Action/SendEmail.pm:
+
+       Snapshot; I'm not finished debugging yet, but this should (hopefully) be
+       very close to actually work.
+       
+2000-04-06 11:38  tobiasb
+
+       * docs/FAQ:
+
+       this clearly needs a lot more work ...
+       
+2000-04-06 11:31  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       ...ah, this felt good.
+       
+       Checked the recent version of Mailtools at CPAN, so I used the "right"
+       method for sending emails.
+       
+       I also added back again the SetRecipients method, and documented it in
+       the POD.
+       
+       I think I will get this working in a couple of hours, so please don't
+       disturb me saying that you disagree very much to those changes.. :)
+       
+2000-04-06 10:46  tobiasb
+
+       * etc/schema.mysql:
+
+       Bugfixing
+       
+2000-04-06 10:40  tobiasb
+
+       * etc/schema.mysql:
+
+       I think that I will get this up working during the next few hours.
+       
+       If I eventually get it working, I'd really appreciate that you don't
+       wreck it until the web interface is working and until we've discussed
+       the design thoroughly and come down to some consensus about it.
+       
+2000-04-06 06:22  tobiasb
+
+       * webrt/Elements/Checkbox:
+
+       This is not my turf ... and I don't know what I'm doing ... anyway, it didn't work, but now it works (?) :)
+       
+2000-04-06 01:49  jesse
+
+       * lib/RT/Action/SendEmailOnResolve.pm:
+
+       file SendEmailOnResolve.pm was initially added on branch rt-1-1.
+       
+2000-04-06 01:49  jesse
+
+       * Makefile, etc/schema.mysql, lib/RT/Template.pm,
+       lib/RT/Action/MailComment.pm, lib/RT/Action/MailCorrespondence.pm,
+       lib/RT/Action/Notify.pm, lib/RT/Action/NotifyOnResolve.pm,
+       lib/RT/Action/SendEmail.pm, lib/RT/Action/SendEmailOnResolve.pm:
+
+       Ok, i think i significantly cut down the complexity of the actions we've got
+       so far. I nuked the old notify rules, as they were superceeded by my more
+       capable SendEmail.pm There are still a few things I'm not happy with...
+       like, the way that scrips, scripscopes and templates are related. it needs
+       some thought and cleanup. but i think it'll work just fine for now.
+       
+       I redid the templates and scrips to fit with the revised actions and added
+       some more docs to SendEmail.pm
+       
+       the generic RT::Action should deal with templates, not SendEmail, I think,
+       but I'm leaving that abstraction for another day.
+       
+       Oh, the Argument from the scrip is now passed into the template as $T::Argument.
+       
+2000-04-05 23:24  jesse
+
+       * webrt/Elements/Checkbox:
+
+       stylistic cleanup
+       using an explicit defined ($foo)
+       
+2000-04-05 18:02  tobiasb
+
+       * webrt/: Elements/Checkbox, Search/PickRestriction:
+
+       Only minor corrections
+       
+2000-04-05 17:43  tobiasb
+
+       * webrt/Search/PickRestriction:
+
+       It certainly doesn't work okay ... I think I should look into it a bit ...
+       
+2000-04-05 17:08  tobiasb
+
+       * webrt/Search/PickRestriction:
+
+       Added ugly submit button
+       
+2000-04-05 10:34  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Temporary implementation of kill
+       
+       As mentionated earlier, I think the right thing is to just set status
+       'dead' and then eventually do some garbage collection later.
+       
+2000-04-05 08:55  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       More error handling
+       
+2000-04-05 08:51  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       added some comments around the dies pointing out that we need better error handling.
+       
+2000-04-05 05:34  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       Doh...I'm really stupid sometimes...
+       
+2000-04-05 05:31  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       For some weird reason (perl 5.6.0 bug?) it wouldn't work here without this fix.
+       
+2000-04-05 00:35  jesse
+
+       * docs/design_docs/acls:
+
+       initial musings on acls for 2.0
+       
+2000-04-05 00:35  jesse
+
+       * docs/design_docs/acls:
+
+       file acls was initially added on branch rt-1-1.
+       
+2000-04-04 09:52  tobiasb
+
+       * etc/schema.mysql:
+
+       Language idea
+       
+2000-04-04 02:17  jesse
+
+       * Makefile, README, bin/rtmux.pl, etc/config.pm:
+
+       The first little bits of structure for future L10n support
+       
+2000-04-03 12:40  tobiasb
+
+       * Makefile, bin/rtmux.pl:
+
+       Cleaned out yet some more visible configuration bugs
+       
+2000-04-03 12:29  tobiasb
+
+       * README:
+
+       Some fixes
+       
+2000-04-03 10:44  tobiasb
+
+       * Makefile, bin/rtmux.pl:
+
+       Just a configuration fix.
+       
+2000-04-03 01:26  jesse
+
+       * Makefile, bin/rtmux.pl:
+
+       Ok, I've modified rtmux.pl and Makefile to add in the first bits of stub
+       code for running Mason as a cgi. it compiles and appears to invoke.
+       but I haven't tested it under a webserver. I've gotta get to sleep now, but
+       figured this would give tobias a leg up with his testing today.
+       
+       Subject:      RE: [Mason] Mason under CGI (was: replacing storyserver)
+       Author:       Ilmari Karonen <iltzu@sci.fi>
+       Date:         Fri, 17 Sep 1999 15:10:20 +0300 (EET DST)
+       
+       On Thu, 16 Sep 1999, Ben Munoz wrote:
+       > I'm running Apache 1.3.6 on NT.  I'm looking to install HTML::Mason and see
+       > what it can do, but from what I've read, I can't install mod_perl with the
+       > current version of ActiveState's Perl, which I have installed on my machine.
+       
+       I realize this is probably not what you were asking for, but I'd like to
+       mention here my recent (yesterday) experience in setting up Mason under
+       plain ol' CGI.
+       
+       Setting: One Mason top level component plus a few subcomponents on a
+       development server with mod_perl and Mason. One production server with the
+       least possible selection of modules. (Not even mod_so!)
+       
+       Needed to quickly get the Mason component up and running on the second
+       server. Installing Mason itself and a few other perl modules was easy.
+       But adding mod_perl was definitely out of the question.
+       
+       What I did was take the usual handler.pl, copy it to cgi-bin, deleting
+       everything from "my $ah = new HTML::Mason::ApacheHandler" onward
+       inclusive, and replacing it with the following:
+       
+           use CGI;
+           my $q = new CGI;
+       
+           # This routine comes from ApacheHandler.pm:
+           my (%args);
+           foreach my $key ( $q->param ) {
+             foreach my $value ( $q->param($key) ) {
+               if (exists($args{$key})) {
+                 if (ref($args{$key})) {
+                   $args{$key} = [@{$args{$key}}, $value];
+                 } else {
+                   $args{$key} = [$args{$key}, $value];
+                 }
+               } else {
+                 $args{$key} = $value;
+               }
+           }
+       
+           my $comp = $ENV{'PATH_TRANSLATED'};
+           my $root = $interp->comp_root;
+           $comp ~= s/^$root//  or die "Component outside comp_root";
+       
+           $interp->exec($comp, %args);
+       
+       This, plus an Action line in httpd.conf mapping *.mas to this script, has
+       so far done the job just fine.
+       
+       One problem I noticed was that this all would've been much harder if I
+       hadn't been familiar with the way ApacheHandler.pm uses CGI.pm and hadn't
+       known where to find that parsing routine. IMHO it'd be a good idea to
+       separate this loop into a user-callable subroutine.
+       
+       (Note: this was Mason 0.6.mumble - I have no experience with the newer
+       versions.)
+       
+       --
+       Ilmari Karonen (iltzu@sci.fi)
+       http://www.sci.fi/~iltzu/
+       
+       _______________________________________________
+       Mason maillist  -  Mason@netizen.com.au
+       http://netizen.com.au/mailman/listinfo/mason
+       
+2000-04-03 00:49  jesse
+
+       * bin/rtmux.pl, etc/schema.mysql, lib/RT/Action.pm,
+       lib/RT/CurrentUser.pm, lib/RT/Scrip.pm, lib/RT/ScripScopes.pm,
+       lib/RT/Template.pm, lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       lib/RT/Watchers.pm, lib/RT/Action/AutoReply.pm,
+       lib/RT/Action/SendEmail.pm:
+
+       Well, that was an exciting afternoon! Er, an exciting evening!
+       *grin* AutoReply now works. which means that all the infrastructure for
+       scrips is back in place.  It required refactoring things a bit. as cool
+       as making RT::Template a MIME::Entity seemed, I just couldn't get it to work
+       right, so now, it just creates a MIME::Entity when it needs it, which is
+       probably better anyway.  I simplified templates a bit. Now they only have
+       access to the Ticket and Transaction. This may not be wise, but we'll see.
+       
+       Also, I merged the two blobs per template (headers and body).
+       
+2000-03-31 08:25  tobiasb
+
+       * bin/webmux.pl:
+
+       rmvd dupl line
+       
+2000-03-31 02:47  jesse
+
+       * Makefile:
+
+       removed old webui hooks from the makefile. bumped the version to 1.3.0
+       
+2000-03-31 02:32  jesse
+
+       * webrt/Elements/Error:
+
+       file Error was initially added on branch rt-1-1.
+       
+2000-03-31 02:32  jesse
+
+       * webrt/Ticket/DisplayTicket:
+
+       file DisplayTicket was initially added on branch rt-1-1.
+       
+2000-03-31 02:32  jesse
+
+       * webrt/: Elements/Error, Elements/SelectStatus,
+       Ticket/Display.html, Ticket/DisplayTicket,
+       Ticket/DisplayTransaction, Ticket/ProcessUpdate.html,
+       Ticket/Update.html:
+
+       Ticket/Update.html will now allow you to submit a ticket update.
+       It's the RT2 equivalent of RT1.0's "comment" and "correspond" pages...
+       with a bit of added value. it's not a global "modify ticket" page. that comes
+       next. but it's where 90% of your single-ticket action will take place.
+       
+2000-03-31 02:28  jesse
+
+       * lib/RT/Ticket.pm:
+
+       TimeTaken should increment now
+       
+2000-03-31 02:26  jesse
+
+       * README:
+
+       added the mason dependency to the README
+       
+2000-03-31 02:25  jesse
+
+       * bin/webmux.pl:
+
+       added some uses to the webmux so I don't have to add them to each page that
+       calls them
+       
+2000-03-30 00:44  jesse
+
+       * webrt/Search/PickRestriction:
+
+       made the variable names in PickRestriction look more like the parameter names
+       in EasySearch. Wanna guess why?
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Search/RestrictSearch.html:
+
+       file RestrictSearch.html was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Search/PickRestriction:
+
+       file PickRestriction was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/Footer:
+
+       file Footer was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/: Elements/Checkbox, Elements/Footer, Elements/Header,
+       Elements/SelectBoolean, Elements/SelectDate,
+       Elements/SelectDateRelation, Elements/SelectOwner,
+       Elements/SelectQueue, Elements/SelectStatus, Elements/dayMenu,
+       Elements/monthMenu, Elements/yearMenu, Search/PickRestriction,
+       Search/RestrictSearch.html:
+
+       A bunch of infrasturcture work on the Ticket Search interface. it doesn't
+       have any real logic behind it. that's next.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/SelectBoolean:
+
+       file SelectBoolean was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/dayMenu:
+
+       file dayMenu was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/SelectStatus:
+
+       file SelectStatus was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/SelectQueue:
+
+       file SelectQueue was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/yearMenu:
+
+       file yearMenu was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/Checkbox:
+
+       file Checkbox was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/Header:
+
+       file Header was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/SelectOwner:
+
+       file SelectOwner was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/monthMenu:
+
+       file monthMenu was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/SelectDateRelation:
+
+       file SelectDateRelation was initially added on branch rt-1-1.
+       
+2000-03-29 23:52  jesse
+
+       * webrt/Elements/SelectDate:
+
+       file SelectDate was initially added on branch rt-1-1.
+       
+2000-03-29 00:33  jesse
+
+       * etc/schema.mysql:
+
+       added a generic "RefersTo" ticket type to the comments.
+       changed the types of Link.Base and Link.Target to VARCHAR(255) from INT(11)
+       
+2000-03-28 05:16  tobiasb
+
+       * README, tools/test:
+
+       darn, it seems like I'll need to install mod-perl ... actually I haven't done that earlier..
+       
+2000-03-28 00:13  jesse
+
+       * Makefile, lib/RT/Record.pm, lib/RT/Transaction.pm,
+       webrt/Search/Listing.html, webrt/Search/QueueItem:
+
+       webrt/Search/Listing.html now gives a basic listing of all open tickets
+       It even has links to the _still display only_ Display.html for each ticket
+       
+2000-03-27 23:55  jesse
+
+       * webrt/Ticket/DisplayTransaction:
+
+       missed this one. sorry.
+       
+2000-03-27 23:55  jesse
+
+       * webrt/Ticket/DisplayTransaction:
+
+       file DisplayTransaction was initially added on branch rt-1-1.
+       
+2000-03-27 23:54  jesse
+
+       * webrt/Ticket/: Display.html, DisplayHistory:
+
+       Hey. Look Ticket/Display.html?id=<int> works!
+       
+2000-03-27 22:35  jesse
+
+       * webrt/Ticket/ToolBar:
+
+       file ToolBar was initially added on branch rt-1-1.
+       
+2000-03-27 22:35  jesse
+
+       * webrt/Ticket/DisplaySummary:
+
+       file DisplaySummary was initially added on branch rt-1-1.
+       
+2000-03-27 22:35  jesse
+
+       * webrt/Ticket/DisplayHistory:
+
+       file DisplayHistory was initially added on branch rt-1-1.
+       
+2000-03-27 22:35  jesse
+
+       * README, webrt/Ticket/DisplayHistory, webrt/Ticket/DisplaySummary,
+       webrt/Ticket/ToolBar:
+
+       Work on WebRT, including a README update!
+       
+       check out  http://s.ly/~jesse/2000_03_28_171606_shot.jpg
+       
+2000-03-27 22:31  jesse
+
+       * lib/RT/Watchers.pm:
+
+       Bugfix in Watchers AdministrativeCc -> AdminCc
+       
+2000-03-27 22:30  jesse
+
+       * webrt/Ticket/: Display.html, Displaysummary, Update.html:
+
+       Work on WebUI. Display.html has slight functionality
+       
+2000-03-27 22:13  tobiasb
+
+       * docs/design_docs/basic-definitions.txt, tools/test:
+
+       minor
+       
+2000-03-27 21:50  tobiasb
+
+       * tools/test:
+
+       added some stuff to the test script
+       
+2000-03-27 21:32  tobiasb
+
+       * lib/RT/: ScripScope.pm, Transaction.pm:
+
+       bugfixing
+       
+2000-03-27 21:22  tobiasb
+
+       * lib/RT/Transaction.pm:
+
+       ...well, even more bugs...
+       
+2000-03-27 21:09  tobiasb
+
+       * lib/RT/: Scrip.pm, ScripScope.pm, Template.pm, Transaction.pm:
+
+       First level bugfixing completed; Now the create (from the cli) calls up the action (though as earlier mentionated no emails are sent).
+       
+2000-03-27 20:54  tobiasb
+
+       * lib/RT/Scrip.pm:
+
+       bgfx
+       
+2000-03-27 20:47  tobiasb
+
+       * lib/RT/Scrip.pm:
+
+       Bugfix
+       
+2000-03-27 20:22  tobiasb
+
+       * lib/RT/ScripScope.pm:
+
+       bugfix
+       
+2000-03-27 20:19  tobiasb
+
+       * docs/design_docs/basic-definitions.txt:
+
+       clarified
+       
+2000-03-27 18:58  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       The recipients aren't set anywhere as far as I can see ... and I'm really uncertain what you've thought of with SetEnvelopeTo vs SetTo, etc.  You'd better take a look.  Obviously, this won't work until the recipients are set somehow
+       
+2000-03-27 18:40  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       I've mostly only added comments.  Anyway, I must say that I like this
+       code. (maybe I'll say something different when I've tried debugging it
+       ;)
+       
+2000-03-27 17:59  tobiasb
+
+       * lib/RT/ScripScope.pm:
+
+       bugfix
+       
+2000-03-27 17:57  tobiasb
+
+       * lib/RT/Action.pm:
+
+       comment/folding bugfix
+       
+2000-03-27 17:56  tobiasb
+
+       * docs/design_docs/basic-definitions.txt:
+
+       file basic-definitions.txt was initially added on branch rt-1-1.
+       
+2000-03-27 17:56  tobiasb
+
+       * docs/design_docs/basic-definitions.txt:
+
+       (no comment :)
+       
+2000-03-27 17:55  tobiasb
+
+       * docs/design_docs/link-definitions.txt:
+
+       I think maybe those additions might be appreciated by Rouillard ... and this was (hopefully) the last thing I do with design documentation/discussion until 1.3 is finished :)
+       
+2000-03-27 17:33  tobiasb
+
+       * docs/design_docs/link-definitions.txt:
+
+       Doh ... I really shouldn't waste my time on this now ...
+       
+2000-03-27 17:17  tobiasb
+
+       * lib/RT/Scrip.pm:
+
+       Did you ever try to see if those modules would pass `perl -c'? :)
+       
+2000-03-27 16:44  jesse
+
+       * etc/schema.mysql, lib/RT/Action.pm, lib/RT/Scrip.pm,
+       lib/RT/ScripScope.pm, lib/RT/Action/AutoReply.pm,
+       lib/RT/Action/SendEmail.pm:
+
+       Lots of works on Scrip, ScripScope, Action. Tobias: wanna take a look?
+       Basically, now Templates are autoloaded by RT::Scrip.pm and passed into
+       the Action as TemplateObj.  Also, Templates are now of type MIME::Entity,
+       which means that they have all the attributes of mail messages and can
+       just be sent.
+       
+2000-03-27 16:20  tobiasb
+
+       * docs/design_docs/: TransactionTypes.txt, link-definitions.txt:
+
+       Linking definitions
+       
+2000-03-27 16:20  tobiasb
+
+       * docs/design_docs/link-definitions.txt:
+
+       file link-definitions.txt was initially added on branch rt-1-1.
+       
+2000-03-27 16:01  tobiasb
+
+       * docs/README.docs:
+
+       Updated the README to reflect that the docs are old :)
+       
+2000-03-27 15:45  tobiasb
+
+       * README:
+
+       status update
+       
+2000-03-27 15:41  tobiasb
+
+       * lib/RT/ScripScope.pm:
+
+       minor string fix
+       
+2000-03-27 15:36  tobiasb
+
+       * docs/design_docs/subscription-definitions.txt:
+
+       How does the system determinate whom to send mail to?
+       
+       The ScripScope table in the DB should indicate whether a Scrip is relevant
+       for a queue or not /* TobiX thinks that this might eventually be extended to
+       keywords, tickets, etc, and not only Queues */ ... the Scope table should
+       indicate whether the Scrip is relevant for a given transaction type ... then
+       the given Action should determinate whether it applies or not, and finally
+       the Action has to find out (via the Watchers table) whom it applies to, and
+       how to contact them ... and the Template tells how the mails that are sent
+       out should look like.
+       
+2000-03-27 12:33  jesse
+
+       * lib/RT/ScripScopes.pm:
+
+       file ScripScopes.pm was initially added on branch rt-1-1.
+       
+2000-03-27 12:33  jesse
+
+       * lib/RT/: ScripScope.pm, ScripScopes.pm:
+
+       a bit more stub work on ScripScopes for tobias.
+       more work after lunch
+       
+2000-03-27 07:18  tobiasb
+
+       * lib/RT/ScripScope.pm:
+
+       test..?
+       
+2000-03-27 07:04  tobiasb
+
+       * lib/RT/: ScripScope.pm, Scrips.pm:
+
+       Initiated work to get the code understand the Scrips/Scripscope split
+       
+2000-03-27 07:04  tobiasb
+
+       * lib/RT/ScripScope.pm:
+
+       file ScripScope.pm was initially added on branch rt-1-1.
+       
+2000-03-27 06:55  tobiasb
+
+       * etc/schema.mysql:
+
+       Added yet a stupid design thought as a comment:
+       
+       # {{{ TABLE ScripScope
+         (...)
+         Queue INT(11), #Queue Id 0 for global
+                        # (maybe there might be conditions where otherb
+                        # Scopes apply, i.e. a ticket, keyword, owner, etc?)
+       
+2000-03-27 00:45  jesse
+
+       * lib/RT/Action/Notify.pm:
+
+       Oops. forgot to commit Notify.pm
+       
+2000-03-27 00:45  jesse
+
+       * lib/RT/Action/Notify.pm:
+
+       file Notify.pm was initially added on branch rt-1-1.
+       
+2000-03-27 00:44  jesse
+
+       * lib/RT/Action/NotifyOnResolve.pm:
+
+       file NotifyOnResolve.pm was initially added on branch rt-1-1.
+       
+2000-03-27 00:44  jesse
+
+       * etc/schema.mysql, lib/RT/Ticket.pm,
+       lib/RT/Action/NotifyOnResolve.pm, lib/RT/Action/NotifyWatchers.pm:
+
+       Work on Notify (Formerly NotifyWatchers)
+       Work on NotifyOnResolve (Formerly NotifyWatchersOnResolve)
+       
+       They don't _work_ per se. But they're getting closer. I'm wondering
+       whether we want to do something smarter with all the addresses we're passing
+       in to make it easier to remove specific ones...for example, we currently
+       have no way to _not_ send mail to the reuqestor.
+       
+2000-03-26 18:13  jesse
+
+       * lib/RT/Action/: AutoReply.pm, SendEmail.pm:
+
+       AutoReply is now a proper subclass of SendEmail.pm
+       Maybe tonight, I'll try to get through some work on the Scrips and ScripScope stuff to make it work again.
+       then to wrok on NotifyWatchers.pm
+       
+2000-03-25 01:03  jesse
+
+       * Makefile, lib/RT/Template.pm, lib/RT/Ticket.pm,
+       lib/RT/Action/SendEmail.pm:
+
+       Work on the templating system. I actually gutted Template.pm and SendEmail.pm
+       (sorry tobix :/)  Basically RT::Template is now A MIME::Entity object, which
+       means that the various things one can do to mail messages can now be done to
+       RT::Template objects, once you run $TemplateObj->Parse on them..
+       SendEmail was cleaned up somewhat to be less intimidating to people who aren't
+       as comfortable with big chunks of fairly dense perl.  Oh and it's a heck of a
+       lot more subclassable now ;)
+       
+       Fixed a tiny bug in Ticket.pm resulting from the changes to Watchers.
+       
+2000-03-24 08:29  tobiasb
+
+       * tools/test:
+
+       I'm making a test suite which is intended to be run off from the Makefile after an install
+       
+2000-03-24 08:29  tobiasb
+
+       * tools/test:
+
+       file test was initially added on branch rt-1-1.
+       
+2000-03-24 08:06  tobiasb
+
+       * etc/schema.mysql:
+
+       Added some more noise
+       
+2000-03-23 23:41  jesse
+
+       * subscription-definitions.txt,
+       docs/design_docs/subscription-definitions.txt, etc/schema.mysql,
+       lib/RT/Action/NotifyWatchers.pm, lib/RT/Action/SendEmail.pm:
+
+       A little bit of cleanup to the SendEmail and NotifyWatchers actions.
+       I probably did more damage than good, but that's what I get for messing
+       with code that makes little sense to me.
+       
+2000-03-23 23:41  jesse
+
+       * docs/design_docs/subscription-definitions.txt:
+
+       file subscription-definitions.txt was initially added on branch rt-1-1.
+       
+2000-03-23 22:19  jesse
+
+       * webrt/Search/BuildSearch:
+
+       Testing the setgid repository. commiting an empty file
+       
+2000-03-23 22:19  jesse
+
+       * webrt/Search/BuildSearch:
+
+       file BuildSearch was initially added on branch rt-1-1.
+       
+2000-03-23 00:50  jesse
+
+       * webrt/Search/QueueFooter:
+
+       file QueueFooter was initially added on branch rt-1-1.
+       
+2000-03-23 00:50  jesse
+
+       * webrt/Search/QueueItem:
+
+       file QueueItem was initially added on branch rt-1-1.
+       
+2000-03-23 00:50  jesse
+
+       * webrt/Search/QueueHeader:
+
+       file QueueHeader was initially added on branch rt-1-1.
+       
+2000-03-23 00:50  jesse
+
+       * webrt/Admin/ModifyUser:
+
+       file ModifyUser was initially added on branch rt-1-1.
+       
+2000-03-23 00:50  jesse
+
+       * lib/RT/Action/NotifyWatchers.pm:
+
+       file NotifyWatchers.pm was initially added on branch rt-1-1.
+       
+2000-03-23 00:50  jesse
+
+       * Makefile, bin/rtmux.pl, etc/schema.mysql, lib/RT/Queue.pm,
+       lib/RT/Scrip.pm, lib/RT/Ticket.pm, lib/RT/Watcher.pm,
+       lib/RT/Action/NotifyWatchers.pm, webrt/Admin/ModifyUser,
+       webrt/Search/Listing.html, webrt/Search/QueueFooter,
+       webrt/Search/QueueHeader, webrt/Search/QueueItem,
+       webrt/Ticket/Displaysummary:
+
+       Did a bit of hacking on webrt2
+       Redid the watchers system according to what I posted earlier to rt-devel
+       Scrips have now broken..but we'll get them working again really soon.
+       Queue Cc and AdminCc now exist in cli adminrt.
+       
+2000-03-22 14:56  tobiasb
+
+       * subscription-definitions.txt:
+
+       definitions in the subscription system
+       
+2000-03-22 14:56  tobiasb
+
+       * subscription-definitions.txt:
+
+       file subscription-definitions.txt was initially added on branch rt-1-1.
+       
+2000-03-22 14:42  tobiasb
+
+       * etc/schema.mysql:
+
+       This is a suggestion about a simple splitting of the Scrips table.  I
+       think the admin tools will be significantly easier to set up if we do
+       it this way.  An administrator can through some UI just add and remove
+       Scrips from a queue.
+       
+2000-03-22 01:02  jesse
+
+       * lib/RT/: ACE.pm, ACL.pm, Action.pm, Area.pm, Areas.pm,
+       Attachment.pm, Attachments.pm, CurrentUser.pm, EasySearch.pm,
+       Queue.pm, Queues.pm, Record.pm, Scrip.pm, Scrips.pm, Template.pm,
+       Templates.pm, Ticket.pm, Tickets.pm, Transaction.pm,
+       Transactions.pm, User.pm, Users.pm, Utils.pm, Watcher.pm,
+       Watchers.pm, Action/AutoReply.pm, Action/MailComment.pm,
+       Action/SendEmail.pm, Interface/Email.pm:
+
+       This may look like a lot of code. but it's not.
+       It's merely the result of running
+       
+       perl -pi.bak -e 's/sub (.*?){\n/# {{{ sub $1\nsub $1 {\n/; s/^}$/}\n# }}}/'
+       
+       basically, this adds # {{{ sub function
+                        and # }}}
+       around all the function names. The main reason to do this is for
+       emacs fold-minor-mode.  It makes things a lot easier to read.
+       In an ideal world, we'd have perl-aware folding editor and there'd be
+       no need for this markup
+       
+2000-03-21 20:38  jesse
+
+       * lib/RT/: Queue.pm, Ticket.pm, Watcher.pm, Watchers.pm:
+
+       Work on watchers.
+       
+2000-03-20 15:21  jesse
+
+       * etc/schema.mysql:
+
+       Modified the schema for my "New Improved Watchers" system. Basically,
+       now you'll be able to tie a template to a given watch. along with a
+       boolean "send mail" flag.  We'll need a special "Mail Watchers" action to
+       make this all work..but the code should be pretty easy.
+       
+               jesse
+       
+2000-03-20 05:27  tobiasb
+
+       * bin/initdb.mysql:
+
+       bugfix
+       
+2000-03-17 15:20  jesse
+
+       * webrt/Ticket/Create.html:
+
+       file Create.html was initially added on branch rt-1-1.
+       
+2000-03-17 15:20  jesse
+
+       * webrt/Ticket/DisplayHeader:
+
+       file DisplayHeader was initially added on branch rt-1-1.
+       
+2000-03-17 15:20  jesse
+
+       * webrt/Ticket/SetOwner:
+
+       file SetOwner was initially added on branch rt-1-1.
+       
+2000-03-17 15:20  jesse
+
+       * webrt/Ticket/SetStatus:
+
+       file SetStatus was initially added on branch rt-1-1.
+       
+2000-03-17 15:20  jesse
+
+       * webrt/Ticket/: Create.html, Display.html, DisplayHeader,
+       Displaysummary, SetOwner, SetStatus, Update.html:
+
+       More work on the basic webui
+       
+2000-03-17 15:20  jesse
+
+       * webrt/Ticket/Displaysummary:
+
+       file Displaysummary was initially added on branch rt-1-1.
+       
+2000-03-17 11:32  tobiasb
+
+       * bin/initdb.mysql:
+
+       Bugfix, the password passing didn't work as it should.
+       
+2000-03-17 10:52  tobiasb
+
+       * etc/schema.mysql:
+
+       - Slashed away the old "MailQueueMembersOn..." junk.
+       
+       - Added Scrips to the Watchers table.  ... the Watchers/Scrips system
+       seems quite flexible .. BUT .. I have a certain feeling that it might
+       be hard to make administration tools that helps a non-hacking
+       administrator to find out which mails are sent where, etc.  Well,
+       that's something we'll have to worry about later :)
+       
+2000-03-17 08:47  tobiasb
+
+       * Makefile, etc/schema.mysql:
+
+       removed some instanses of 'su' that really shouldn't be needed, and which also broke on anything but GNU shellutils
+       
+2000-03-17 07:32  tobiasb
+
+       * README:
+
+       Some additions and changes to reflect that this is, after all, a
+       development version.  Meri, do you subscribe to cvs-commit? :)
+       
+2000-03-17 06:01  tobiasb
+
+       * etc/schema.mysql:
+
+       # Those subscription field are not used in RT 1.1 as I see it.
+       # I think we should modify the Scrips system so it by default
+       # respects those fields.
+        MailOwnerOnTransaction INT,             # notify owner on transaction
+        MailMembersOnTransaction INT,          # notify list members on transaction
+        MailRequestorOnTransaction INT,        # notify requestor on transaction
+        MailRequestorOnCreation INT,           # notify user on creation
+        MailMembersOnCorrespondence INT,               # notify members on creation
+        MailMembersOnComment INT,              # notify members on comment
+       
+2000-03-16 16:31  jesse
+
+       * webrt/: Search/Listing.html, Ticket/Display.html,
+       Ticket/Modify.html, Ticket/ProcessUpdate.html, Ticket/Update.html,
+       Ticket/ValidateUpdate.html:
+
+       The beginnings of the All New! Web UI based around HTML::Mason
+       There's nothing functional here yet. but it's a start at a framework.
+       Ticket/Update.html is probably the closest to "real" code so far.
+       
+2000-03-16 16:31  jesse
+
+       * webrt/Ticket/ValidateUpdate.html:
+
+       file ValidateUpdate.html was initially added on branch rt-1-1.
+       
+2000-03-16 16:31  jesse
+
+       * webrt/Ticket/Display.html:
+
+       file Display.html was initially added on branch rt-1-1.
+       
+2000-03-16 16:31  jesse
+
+       * webrt/Ticket/Modify.html:
+
+       file Modify.html was initially added on branch rt-1-1.
+       
+2000-03-16 16:31  jesse
+
+       * webrt/Search/Listing.html:
+
+       file Listing.html was initially added on branch rt-1-1.
+       
+2000-03-16 16:31  jesse
+
+       * webrt/Ticket/ProcessUpdate.html:
+
+       file ProcessUpdate.html was initially added on branch rt-1-1.
+       
+2000-03-16 16:31  jesse
+
+       * webrt/Ticket/Update.html:
+
+       file Update.html was initially added on branch rt-1-1.
+       
+2000-03-16 14:46  tobiasb
+
+       * Makefile:
+
+       Changed the default database name from rt to RT to avoid confusion with previously installed versions of rt
+       
+2000-03-16 02:34  tobiasb
+
+       * lib/RT/User.pm:
+
+       Bugfixes:  It was impossible not to give requests to somebody.
+       
+2000-03-16 01:32  tobiasb
+
+       * lib/RT/Interface/Email.pm:
+
+       Bugfixes
+       
+       was
+       
+         if ($CurrentUser->Id == 0) {
+           #If it fails, create a user
+       
+       now
+       
+         unless ($CurrentUser->Id) {
+           #If it fails, create a user
+       
+       as Id returned undef.
+       
+       Changed 'Queue' to 'QueueTag'.
+       
+       Added some few comments
+       
+2000-03-14 00:40  jesse
+
+       * README:
+
+       Modified the README for the beginnings of the new webui
+       
+2000-03-14 00:39  jesse
+
+       * bin/: rtmux.pl, webmux.pl:
+
+       added webmux. modified rtmux.pl to clean out some cruft
+       
+2000-03-14 00:39  jesse
+
+       * bin/webmux.pl:
+
+       file webmux.pl was initially added on branch rt-1-1.
+       
+2000-03-13 12:53  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       rt-mailgate has now recieved and logged correspondence and comments with current schema.
+       
+2000-03-08 23:26  jesse
+
+       * lib/RT/: Ticket.pm, Transaction.pm:
+
+       DBIx cleanups to deal with load by id when id is null
+       Work on comment and correspond. Eventually, we'll get the
+       per-transaction cc and bcc working.
+       
+2000-03-08 18:22  jesse
+
+       * bin/rtmux.pl, lib/RT/Queue.pm, lib/RT/Ticket.pm,
+       lib/RT/Transaction.pm, lib/RT/User.pm:
+
+       rtq works a bit better
+       rtadmin works again.
+       rtadmin -user create
+               -user modify
+        both seem to work ok
+       
+       rt -create now makes sure a queue exists and permits ticket creation
+       rt -create now make sure a prospective owner has queue membership.
+       
+2000-03-02 17:23  tobiasb
+
+       * lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       lib/RT/Action/SendEmail.pm, etc/schema.mysql:
+
+       resolve, open, take (if unowned, at least) seems to work
+       
+2000-03-02 16:10  tobiasb
+
+       * lib/RT/: Ticket.pm, Ticket.pm:
+
+       Bugfixing
+       
+2000-03-02 14:35  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Correspondence almost works
+       
+2000-03-02 13:26  tobiasb
+
+       * lib/RT/Ticket.pm, lib/RT/Action/MailComment.pm, etc/schema.mysql:
+
+       Comments almost work now (from the cli)
+       
+2000-03-02 12:38  tobiasb
+
+       * lib/RT/Action/: MailComment.pm, MailCorrespondence.pm:
+
+       one minor bugfix + some comments
+       
+2000-03-01 23:58  jesse
+
+       * NEWS:
+
+       brought the news up to date.
+       nuked old /contrib
+       nuked old /etc/templates.
+       
+       bedtime now.
+       
+2000-03-01 23:57  jesse
+
+       * NEWS:
+
+       brought the news up to date (a bit)
+       
+2000-03-01 23:50  jesse
+
+       * Makefile:
+
+       removed a lot of outdated cruft.
+       Bumped the version to 1.1.11 for a release later this week.
+       
+2000-03-01 23:18  jesse
+
+       * lib/RT/: Ticket.pm, Interface/Email.pm:
+
+       The mail gateway now lets you create, comment and correspond on tickets.
+       Ticket->Owner now deals better when the owner is NULL
+       
+2000-03-01 21:27  jesse
+
+       * HACKING:
+
+       minor comments added to hacking
+       
+2000-03-01 21:20  jesse
+
+       * HACKING:
+
+       Trivial change to HACKING to test commitinfo
+       
+2000-03-01 21:09  jesse
+
+       * lib/RT/Interface/Email.pm:
+
+       file Email.pm was initially added on branch rt-1-1.
+       
+2000-03-01 21:09  jesse
+
+       * bin/rtmux.pl, lib/RT/Interface/Email.pm:
+
+       work on mailgate
+       
+2000-03-01 18:36  tobiasb
+
+       * docs/design_docs/TransactionTypes.txt, etc/schema.mysql:
+
+       Some loose thoughts only ... as comments and a doc which I'm not sure
+       if it's possible to understand by others than me anyway :)
+       
+       Anyway, it is too complex.  We'll just continue following the current design.
+       
+2000-03-01 18:36  tobiasb
+
+       * docs/design_docs/TransactionTypes.txt:
+
+       file TransactionTypes.txt was initially added on branch rt-1-1.
+       
+2000-03-01 17:51  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Seems like it's a lot of things that doesn't work here.
+       
+2000-03-01 16:26  tobiasb
+
+       * etc/schema.mysql:
+
+       Ehm ... maybe I forgot to commit this one.  It contains some fixes.
+       
+2000-03-01 16:08  jesse
+
+       * TODO:
+
+       ripped a bit of obsolete code from rtmux.pl
+       updated the TODO to reference the production instance of RT.
+       
+2000-03-01 14:48  jesse
+
+       * bin/rtmux.pl:
+
+       removed the old web ui hooks from rtmux.pl
+       
+2000-03-01 12:12  tobiasb
+
+       * lib/RT/: Action.pm, Scrip.pm, Template.pm, Ticket.pm,
+       Watchers.pm, Action/AutoReply.pm, Action/SendEmail.pm:
+
+       I've done some debugging and polishing.  I hope it won't break with stuff you're eventually working with
+       
+2000-02-29 16:58  tobiasb
+
+       * lib/RT/: Queue.pm, Ticket.pm, Watchers.pm:
+
+       One sort of bugfix..
+       
+2000-02-29 16:35  tobiasb
+
+       * lib/RT/: Queue.pm, Template.pm, Ticket.pm, Watcher.pm,
+       Watchers.pm, Action/SendEmail.pm:
+
+       Scrips work for me.  Well, I'm not that sure anyway.  I had actually
+       expected one email to drop in for each Scrip - but I only got the
+       AutoReply and the Correspondence.  Maybe I should debug even more.
+       
+2000-02-29 14:29  tobiasb
+
+       * lib/RT/Watchers.pm:
+
+       LimitToTicket updated (not tested, though)
+       LimitToQueue added     (not tested, though)
+       
+2000-02-29 14:27  tobiasb
+
+       * etc/schema.mysql:
+
+       
+       # Primarly used by RT::Actions::SendEmail as well as some subclasses
+       # to determinate whom to send emails to.  This scheme should work out
+       # better than the scheme under RT 1.0, and should suit most users.
+       # For more fine grained control, it's possible to create tables as you
+       # like and make a new subclass of RT::Actions::SendEmail where the
+       # SetRecepients sub is overloaded :)
+       
+       CREATE TABLE Watchers (
+          id int(11) AUTO_INCREMENT PRIMARY KEY,
+          Value int(11),
+          Scope varchar(16), # Might be "Queue" and "Ticket" as for now
+                             # ... might be extended to "Keywords", "Owners", etc.
+          Email VARCHAR(255),
+          Type VARCHAR(16), #Requestor, Cc, Bcc
+          Creator INT(11),
+          Created TIMESTAMP,
+          LastUpdatedBy INT(11),
+          LastUpdated TIMESTAMP
+       )\g
+       
+2000-02-29 13:40  tobiasb
+
+       * etc/schema.mysql:
+
+       CREATE TABLE Watchers (
+          id int(11) AUTO_INCREMENT PRIMARY KEY,
+          Ticket int(11), # 0 for all
+          Queue int(11), # 0 for all
+               # We might consider adding more power here,
+               # i.e. TransactionType, Scrip, etc
+          Email VARCHAR(255),
+          Type VARCHAR(16), #Requestor, Cc, Bcc
+          Creator INT(11),
+          Created TIMESTAMP,
+          LastUpdatedBy INT(11),
+          LastUpdated TIMESTAMP
+       )\g
+       
+2000-02-29 10:43  tobiasb
+
+       * etc/schema.mysql:
+
+       Template.{content and title} => Content and Title - since we use
+       UpperCase in the rest of the DD.
+       
+2000-02-29 10:31  tobiasb
+
+       * etc/schema.mysql:
+
+       Blob, that is..
+       
+2000-02-29 10:12  tobiasb
+
+       * etc/schema.mysql:
+
+       Added ExtraHeaders as VARCHAR(255).  255 characters should be enough
+       for storing some few extra header lines, but anyway maybe it should
+       have been blob instead?
+       
+2000-02-29 09:51  tobiasb
+
+       * lib/RT/: Attachment.pm, Action/AutoReply.pm, Action/SendEmail.pm:
+
+       I've continued the work on mail sending.  There is still some missing
+       stuff:
+       
+       1. EasySearch/Scrips issue - currently it doesn't properly output "
+       all scrips that has (queue=0 or queue=this) and (type="any" or type=this)".
+       
+       2. A serious stub in RT::Action::SendEmail - I don't know where to find
+       "interessted parties".  I think it could be nice putting queue watchers
+       in the same table as ticket watchers.
+       
+2000-02-29 09:42  tobiasb
+
+       * lib/RT/Scrips.pm:
+
+       Hack - everything that applies for a Correspondence action also
+       applies for a Create action (I'd daresay).
+       
+2000-02-29 07:02  tobiasb
+
+       * lib/RT/Transaction.pm:
+
+       Some insignificant changes.  Well, one marginally significant; it
+       shouldn't $Scrip->Commit unless $Scrip->Prepare()
+       
+2000-02-29 04:12  tobiasb
+
+       * etc/schema.mysql:
+
+       I've started at general transaction emails.
+       
+2000-02-29 03:58  tobiasb
+
+       * lib/RT/Watchers.pm:
+
+       Added an Emails method.  It can be used like:
+       
+          print join(",", @{$Watchers->Emails("Requestors")});
+       
+       or
+       
+          $Watchers->LimitToRequestors;
+          $Emails=$Watchers->Emails;
+       
+       ...
+       
+2000-02-29 02:31  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       Sender => RT-Originator
+       
+2000-02-29 00:40  jesse
+
+       * README:
+
+       Added Into Netowrks and Funcom to the thanks at the top of the readme
+       `
+       
+2000-02-28 23:20  jesse
+
+       * Makefile:
+
+       RT 1.1.10 released. Tobix has autoreply basically working :)
+       
+2000-02-28 22:49  jesse
+
+       * Makefile, NEWS, README:
+
+       Fixed a longstanding bug in the WebRT administrator
+       added a note about stripmime to the readme.
+       added a bit of code from curl.com that adds functionality to rtq for reporting
+       
+2000-02-28 22:17  jesse
+
+       * Makefile, NEWS:
+
+       bumped the version for release of 1.0.2
+       
+2000-02-28 11:33  tobiasb
+
+       * lib/RT/: Template.pm, Transaction.pm, Action/AutoReply.pm,
+       Action/SendEmail.pm:
+
+       AutoReply seems to work now ... except that rtadmin didn't set email
+       aliases for the queue, so the mail bounces with invalid from address.
+       I won't look more at this until tomorrow.  You're very welcome to take
+       over for a while if you feel like it :)
+       
+2000-02-28 11:29  tobiasb
+
+       * etc/schema.mysql:
+
+       AutoReply seems to work now
+       
+2000-02-28 08:18  tobiasb
+
+       * HACKING:
+
+       Added some hints and misc.  Please look through.
+       
+2000-02-28 05:25  tobiasb
+
+       * lib/RT/: Ticket.pm, Transaction.pm, Action/AutoReply.pm,
+       Action/SendEmail.pm:
+
+       - The queue tag rather than queue id is delievered from the cli, this
+         broke in Ticket::Create.  Fixed.
+       
+       - The Attachments have to be assigned to a transaction before Scrips
+          is run.  Now the Attachments are delievered as a part of the parameters
+          to Transaction::Create
+       
+2000-02-27 23:28  tobiasb
+
+       * etc/schema.mysql:
+
+       Fixed the AutoReply template ... still not tested, though.
+       
+2000-02-27 22:04  tobiasb
+
+       * lib/RT/Action/: AutoReply.pm, SendEmail.pm:
+
+       I think we can nuke the old rt/lib/rt/support/mail.pm now - everything
+       should be located in SendEmail.pm, AutoReply.pm and Template.pm by now :)
+       
+       It's not tested yet, and the templates needs upgrading.  Anyway, I will
+       probably not mess more around with the perl code as for now.
+       
+2000-02-27 21:43  tobiasb
+
+       * bin/rtmux.pl:
+
+       Seems like $rtversion="!!RT_VERSION!!" had disappeared.  I renamed the
+       variable to $VERSION.
+       
+2000-02-27 21:22  jesse
+
+       * lib/RT/Ticket.pm:
+
+       dates now display in the cli.
+       
+2000-02-27 20:53  tobiasb
+
+       * lib/RT/Templates.pm:
+
+       Fixed DBIx::EasySearch => RT::EasySearch
+       
+2000-02-27 20:53  tobiasb
+
+       * lib/RT/Template.pm:
+
+       Method Template::Parse added, Text::Template used for the moment.
+       
+       (sigh ... that means my next worktask will be to update the
+       templates...)
+       
+2000-02-27 20:49  jesse
+
+       * lib/RT/Ticket.pm:
+
+       so. Ticket->Create now actually figures out ccs and bccs and requestors from the mime
+       object passed in to it.
+       
+2000-02-27 19:05  jesse
+
+       * lib/RT/Transaction.pm:
+
+       added a comment for tobias about a new line that didn't make sense.
+       
+2000-02-27 18:46  jesse
+
+       * lib/RT/: Ticket.pm, Watchers.pm:
+
+       watchers updates.
+       
+2000-02-27 17:25  tobiasb
+
+       * lib/RT/Template.pm:
+
+       file Template.pm was initially added on branch rt-1-1.
+       
+2000-02-27 17:25  tobiasb
+
+       * lib/RT/: Attachment.pm, Template.pm, Templates.pm,
+       Action/AutoReply.pm, Action/SendEmail.pm:
+
+       Development snapshot.  I guess I'll have it working in half an hour
+       with _efficient_ working, that is some four hours working at my
+       current efficiency rate :/
+       
+2000-02-27 17:25  tobiasb
+
+       * lib/RT/Templates.pm:
+
+       file Templates.pm was initially added on branch rt-1-1.
+       
+2000-02-27 16:43  tobiasb
+
+       * etc/schema.mysql:
+
+       Added some templates
+       
+2000-02-27 11:19  tobiasb
+
+       * lib/RT/: Scrip.pm, Transaction.pm:
+
+       development snapshot
+       
+2000-02-27 10:31  tobiasb
+
+       * lib/RT/Action/: AutoReply.pm, MailComment.pm,
+       MailCorrespondence.pm, SendEmail.pm:
+
+       I've started bashing at the mail sending functionallity.  It seemed quite
+       stubbed to me.
+       
+2000-02-24 03:44  tobiasb
+
+       * etc/schema.mysql:
+
+       minor bug in comment
+       
+2000-02-24 03:24  tobiasb
+
+       * lib/RT/Action/SendEmail.pm:
+
+       clearing out a potential weird bug
+       
+2000-02-24 00:27  jesse
+
+       * lib/RT/Scrips.pm:
+
+       Fixed a typo in my code near tobix' changes to Scrips.pm. his changes look good.
+       
+2000-02-24 00:03  jesse
+
+       * Makefile, NEWS, README:
+
+       little fix to mail manipulate from "Heather L. Sherman" <heather@idealab.com>
+       
+2000-02-23 17:06  tobiasb
+
+       * lib/RT/: Scrips.pm, Action/SendEmail.pm:
+
+       Some small fixes.  You'd better look through it, as I'm not completely sure what I'm doing :)
+       
+2000-02-23 16:07  tobiasb
+
+       * etc/schema.mysql:
+
+       An insert entry here reffers to SendMail.pm, while the file is SendEmail.pm
+       
+2000-02-23 15:26  jesse
+
+       * docs/rt-templates.html:
+
+       [no log message]
+       
+2000-02-23 11:38  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Could not get 'rt -create' working.  It seems more sane now, but I'm not really sure what I'm doing ... so you should have a peek at it.  I changed  $Id->SUPER::_Set("EffectiveId",$id); to $self->SUPER::_Set etc
+       
+2000-02-22 23:35  jesse
+
+       * lib/RT/InterestedParty.pm:
+
+       removed vestigal code
+       
+2000-02-22 23:25  jesse
+
+       * etc/schema.mysql, lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       lib/RT/Transactions.pm, lib/RT/Watchers.pm:
+
+       A bit more work on watchers. we're getting close.
+       rtq works today.
+       rt -create basically works.
+       rt -take works.
+       
+       rt -show will now start to show ownership changes.
+       
+2000-02-22 08:06  jesse
+
+       * Makefile, etc/schema.mysql, lib/RT/Ticket.pm:
+
+       Work on Watchers.  Most of the code needed to make this work is now
+       present. sadly I haven't even _tried_ to run it yet.
+       
+2000-02-21 01:33  jesse
+
+       * lib/RT/Watcher.pm:
+
+       file Watcher.pm was initially added on branch rt-1-1.
+       
+2000-02-21 01:33  jesse
+
+       * lib/RT/: InterestedParties.pm, InterestedParties.pm~,
+       Notification.pm, Ticket.pm, Watcher.pm, Watchers.pm:
+
+       Renamed interested parties to "Watchers"
+       Watchers are now keyed by EmailAddress rather than UserId. Not every CC will have an rt account, i think
+       
+2000-02-21 01:33  jesse
+
+       * lib/RT/Watchers.pm:
+
+       file Watchers.pm was initially added on branch rt-1-1.
+       
+2000-02-20 18:28  jesse
+
+       * lib/RT/InterestedParties.pm~:
+
+       file InterestedParties.pm~ was initially added on branch rt-1-1.
+       
+2000-02-20 18:28  jesse
+
+       * lib/RT/InterestedParties.pm:
+
+       file InterestedParties.pm was initially added on branch rt-1-1.
+       
+2000-02-20 18:28  jesse
+
+       * lib/RT/InterestedParty.pm:
+
+       file InterestedParty.pm was initially added on branch rt-1-1.
+       
+2000-02-20 18:28  jesse
+
+       * lib/RT/: InterestedParties.pm, InterestedParties.pm~,
+       InterestedParty.pm:
+
+       [no log message]
+       
+2000-02-20 18:24  jesse
+
+       * etc/schema.mysql, lib/RT/Ticket.pm:
+
+       [no log message]
+       
+2000-02-20 15:27  jesse
+
+       * etc/schema.mysql, lib/RT/Action.pm, lib/RT/Scrip.pm,
+       lib/RT/Action/AutoReply.pm, lib/RT/Action/SendEmail.pm:
+
+       Work on scrips. stubs for the webui
+       
+2000-02-19 17:45  jesse
+
+       * lib/RT/: Action.pm, Scrip.pm:
+
+       work on actions. checking in for eric to take a look
+       
+2000-02-19 16:48  jesse
+
+       * lib/RT/Action/AutoReply.pm:
+
+       file AutoReply.pm was initially added on branch rt-1-1.
+       
+2000-02-19 16:48  jesse
+
+       * lib/RT/Action/MailCorrespondence.pm:
+
+       file MailCorrespondence.pm was initially added on branch rt-1-1.
+       
+2000-02-19 16:48  jesse
+
+       * lib/RT/Action.pm:
+
+       file Action.pm was initially added on branch rt-1-1.
+       
+2000-02-19 16:48  jesse
+
+       * lib/RT/Action/MailComment.pm:
+
+       file MailComment.pm was initially added on branch rt-1-1.
+       
+2000-02-19 16:48  jesse
+
+       * lib/RT/: Action.pm, Action/AutoReply.pm, Action/MailComment.pm,
+       Action/MailCorrespondence.pm, Action/SendEmail.pm:
+
+       As it turns out, Action needed to be abstracted a bit and renamed.
+       
+       I'm also making the Object syntax a bit.
+       
+2000-02-19 16:48  jesse
+
+       * lib/RT/Action/SendEmail.pm:
+
+       file SendEmail.pm was initially added on branch rt-1-1.
+       
+2000-02-17 09:19  jesse
+
+       * etc/schema.mysql, lib/RT/Scrip.pm, lib/RT/Scrips.pm,
+       lib/RT/Transaction.pm:
+
+       The base architecture for Scrips to work should now be in place.
+       Next up: write example "Actions" (gotta move the Scrips directory to Actions)
+       for the scrip handlers to use.
+       
+2000-02-16 09:59  jesse
+
+       * lib/RT/: Scrip.pm, Transaction.pm:
+
+       Starting to make scrips actually work
+       
+2000-02-10 23:24  jesse
+
+       * lib/RT/Scrips.pm:
+
+       file Scrips.pm was initially added on branch rt-1-1.
+       
+2000-02-10 23:24  jesse
+
+       * lib/RT/: Scrip.pm, Scrips.pm, Transaction.pm:
+
+       more work stubbing scrips. but now my arms hurt badly enough that i'm going to log out.
+       
+2000-02-10 23:24  jesse
+
+       * lib/RT/Scrip.pm:
+
+       file Scrip.pm was initially added on branch rt-1-1.
+       
+2000-01-31 02:38  tobiasb
+
+       * README, lib/RT/Attachments.pm:
+
+       does the commit automail work now?
+       
+2000-01-30 23:54  jesse
+
+       * NEWS, README, etc/schema.mysql, lib/RT/CurrentUser.pm,
+       lib/RT/Record.pm, lib/RT/User.pm:
+
+       Work on the User object.
+       The mail gateway now autocreates users. (This will make more sense
+       once we start tying tickets to users by means of the InterestedParties table.
+       
+2000-01-29 01:33  jesse
+
+       * Makefile:
+
+       makefile cleanup. including removing the C compiler and template and
+       transaction directories
+       
+2000-01-29 01:30  jesse
+
+       * Makefile:
+
+       removed C compiler from the makefile
+       
+2000-01-29 01:25  jesse
+
+       * Makefile:
+
+       Removed C compiler from the makefile. we just don't need it anymore
+       
+2000-01-29 01:22  jesse
+
+       * Makefile:
+
+       bumped version to 1-1-8
+       
+2000-01-29 01:20  jesse
+
+       * Makefile:
+
+       removed template and transaction paths from the makefile.
+       (to test new cvs wrappers and cuz they should be gona anyway)
+       
+2000-01-29 00:26  jesse
+
+       * etc/schema.mysql:
+
+       Started to do work on scrips. see the schema for how it would work.
+       
+2000-01-28 20:04  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Some modifications and bugfixes on the messages popping up after a `successful' correspondence/comment
+       
+2000-01-28 19:47  tobiasb
+
+       * lib/RT/Ticket.pm:
+
+       Removed some annoying warnings.  I don't know if this is the "right"
+       way to remove them, but it works (for me, at least), it's an easy
+       way to do it, and it's harmless;
+       
+       instead of writing
+               $var_that_might_be_undef
+       I write
+               $var_that_might_be_undef || ""
+       eventually with parantheses, and eventually with "" replaced by 0, or maybe
+       (not tested) even undef.
+       
+2000-01-28 13:22  tobiasb
+
+       * Makefile:
+
+       Two substitution parameters was forgotten in config.pm
+       
+2000-01-28 13:12  tobiasb
+
+       * etc/schema.mysql:
+
+       Oups, committed the wrong file
+       
+2000-01-28 13:10  tobiasb
+
+       * Makefile, etc/schema.mysql:
+
+       Two substitution parameters was forgotten in config.pm
+       
+2000-01-27 04:09  tobiasb
+
+       * Makefile, bin/initdb.mysql:
+
+       small password fix
+       
+2000-01-25 23:26  jesse
+
+       * Makefile:
+
+       bumped the version to 1.1.7 for tonight's release.
+       
+2000-01-25 23:12  jesse
+
+       * lib/RT/: Attachments.pm, Ticket.pm, Transaction.pm:
+
+       DBIx::EasySearch now has a -> First method. which is like -> Next, but picks
+       the first element
+       
+       DBIx::Record -> now returns '' rather than undef for null values. it's less
+       likely to break interfaces.
+       
+2000-01-25 16:36  tobiasb
+
+       * README:
+
+       aargh
+       
+2000-01-25 16:31  tobiasb
+
+       * TODO:
+
+       test
+       
+2000-01-25 14:57  tobiasb
+
+       * etc/schema.mysql:
+
+       bugfix
+       
+2000-01-25 14:44  tobiasb
+
+       * Makefile:
+
+       removed obsoleteness
+       
+2000-01-25 14:37  tobiasb
+
+       * README:
+
+       The UPGRADE section of the README was inaccurate
+       
+2000-01-25 14:31  tobiasb
+
+       * lib/RT/Transaction.pm:
+
+       comment bug
+       
+2000-01-24 01:45  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+2000-01-24 01:43  jesse
+
+       * Makefile, NEWS, lib/RT/Attachment.pm, lib/RT/Attachments.pm,
+       lib/RT/Record.pm, lib/RT/Ticket.pm, lib/RT/Tickets.pm,
+       lib/RT/Transaction.pm, lib/RT/Transactions.pm:
+
+       
+       * Attachments support in code. rtq now basically works
+         Mailgate creates new tickets
+         rt -show now works a bit (doesn't display transaction content yet)
+         rt -subject works. several other commandline tools work
+       
+2000-01-23 18:32  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+2000-01-23 18:27  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+2000-01-23 18:19  jesse
+
+       * etc/schema.mysql, lib/RT/Attachment.pm, lib/RT/Attachments.pm,
+       lib/RT/Ticket.pm, lib/RT/Transaction.pm:
+
+       Work to make attachments work. So far we've got something that puts attachments
+       in the database.
+       
+2000-01-21 12:25  tobiasb
+
+       * README:
+
+       doc-bugfix
+       
+2000-01-18 23:56  jesse
+
+       * lib/RT/Ticket.pm:
+
+       work on the mail gateway. (mainly disemboweled it)
+       
+2000-01-18 00:34  jesse
+
+       * lib/RT/Attachments.pm:
+
+       file Attachments.pm was initially added on branch rt-1-1.
+       
+2000-01-18 00:34  jesse
+
+       * docs/API:
+
+       file API was initially added on branch rt-1-1.
+       
+2000-01-18 00:34  jesse
+
+       * lib/RT/Attachment.pm:
+
+       file Attachment.pm was initially added on branch rt-1-1.
+       
+2000-01-18 00:34  jesse
+
+       * HACKING:
+
+       file HACKING was initially added on branch rt-1-1.
+       
+2000-01-18 00:34  jesse
+
+       * HACKING, Makefile, docs/API, etc/schema.mysql,
+       lib/RT/Attachment.pm, lib/RT/Attachments.pm, lib/RT/CurrentUser.pm,
+       lib/RT/Queue.pm, lib/RT/Record.pm, lib/RT/Ticket.pm,
+       lib/RT/Transaction.pm, lib/RT/Utils.pm:
+
+       work on attachments.
+       work on structure
+       
+2000-01-17 12:59  jesse
+
+       * Makefile, etc/schema.mysql, lib/RT/ACE.pm, lib/RT/ACL.pm,
+       lib/RT/CurrentUser.pm, lib/RT/Queue.pm, lib/RT/Queues.pm,
+       lib/RT/Ticket.pm, lib/RT/Tickets.pm, lib/RT/Transaction.pm,
+       lib/RT/Transactions.pm, lib/RT/User.pm, lib/RT/Users.pm:
+
+       work on create. work on attachments. upcased the table names.
+       
+2000-01-14 15:34  jesse
+
+       * etc/schema.mysql:
+
+       work on schema
+       
+2000-01-14 14:47  jesse
+
+       * NEWS, etc/schema:
+
+       schema change to make long email addresses work better for queue members.
+       
+2000-01-14 01:07  jesse
+
+       * lib/RT/: EasySearch.pm, Record.pm, Ticket.pm, Tickets.pm,
+       Transaction.pm:
+
+       basic searches now work.
+       comments almost work.
+       creates are having an issue with effectiveid not getting set.
+       
+2000-01-13 18:04  jesse
+
+       * Makefile, lib/RT/EasySearch.pm, lib/RT/Tickets.pm:
+
+       [no log message]
+       
+2000-01-13 15:03  jesse
+
+       * lib/RT/Utils.pm:
+
+       file Utils.pm was initially added on branch rt-1-1.
+       
+2000-01-13 15:03  jesse
+
+       * lib/RT/Utils.pm:
+
+       [no log message]
+       
+2000-01-09 20:38  jesse
+
+       * lib/RT/EasySearch.pm:
+
+       file EasySearch.pm was initially added on branch rt-1-1.
+       
+2000-01-09 20:38  jesse
+
+       * lib/RT/: EasySearch.pm, Queue.pm, Queues.pm, Record.pm,
+       Tickets.pm:
+
+       added RT/EasySearch.pm
+       
+       rtadmin queue -list will now show all queues.
+       removed some debugging output
+       
+2000-01-04 01:21  jesse
+
+       * etc/schema.mysql, lib/RT/CurrentUser.pm, lib/RT/Queue.pm,
+       lib/RT/Record.pm, lib/RT/Ticket.pm, lib/RT/Transaction.pm,
+       lib/RT/User.pm:
+
+       Lots of work. first working ticket update.
+       
+2000-01-04 01:21  jesse
+
+       * lib/RT/CurrentUser.pm:
+
+       file CurrentUser.pm was initially added on branch rt-1-1.
+       
+2000-01-03 07:40  tobiasb
+
+       * README, README:
+
+       bugfix
+       
+2000-01-03 01:39  jesse
+
+       * lib/RT/: Record.pm, Ticket.pm, User.pm:
+
+       basic create support working.
+       more boackend and abstraction work
+       
+2000-01-02 22:04  jesse
+
+       * lib/RT/Ticket.pm:
+
+       [no log message]
+       
+2000-01-02 20:31  jesse
+
+       * lib/RT/: ACE.pm, Area.pm, Queue.pm, Record.pm, Ticket.pm,
+       Transaction.pm, User.pm:
+
+       work on a buit more abstraction
+       
+2000-01-01 19:01  jesse
+
+       * etc/schema.mysql, lib/RT/ACE.pm, lib/RT/Ticket.pm,
+       lib/RT/User.pm:
+
+       work on Ticket.pm, schema.
+       things are starting to use immutable ids when referring to other tables.
+       
+1999-12-30 02:13  jesse
+
+       * Makefile, etc/schema.mysql, lib/RT/Queue.pm, lib/RT/Record.pm,
+       lib/RT/User.pm:
+
+       reworked things to use autoloaded functions.
+       
+1999-12-29 00:24  jesse
+
+       * Makefile, bin/rtmux.pl, etc/schema.mysql, lib/RT/ACE.pm,
+       lib/RT/Area.pm, lib/RT/Queue.pm, lib/RT/Record.pm,
+       lib/RT/Ticket.pm, lib/RT/Transaction.pm, lib/RT/User.pm:
+
+       lots of changes. some of them not quite done perfectly.
+       but admin is closer
+       
+1999-12-28 01:39  jesse
+
+       * bin/rtmux.pl, etc/config.pm, lib/RT/ACE.pm, lib/RT/ACL.pm,
+       lib/RT/Area.pm, lib/RT/Areas.pm, lib/RT/Notification.pm,
+       lib/RT/Queue.pm, lib/RT/Queues.pm, lib/RT/Record.pm,
+       lib/RT/Ticket.pm, lib/RT/Tickets.pm, lib/RT/Transaction.pm,
+       lib/RT/Transactions.pm, lib/RT/User.pm, lib/RT/Users.pm:
+
+       rtadmin will now invoke and exit.
+       
+1999-12-27 19:54  jesse
+
+       * Makefile, bin/initacls.mysql, bin/rtmux.pl, etc/acl.mysql,
+       etc/config.pm, lib/RT/Record.pm:
+
+       first steps toward runability
+       
+1999-12-27 01:51  jesse
+
+       * Makefile, bin/rtmux.pl, etc/config.pm:
+
+       trying to get the config worked out
+       
+1999-12-26 21:58  jesse
+
+       * Makefile, Makefile:
+
+       [no log message]
+       
+1999-12-26 21:55  jesse
+
+       * Makefile, Makefile:
+
+       [no log message]
+       
+1999-12-26 21:53  jesse
+
+       * Makefile, Makefile, Makefile:
+
+       [no log message]
+       
+1999-12-26 21:51  jesse
+
+       * Makefile, Makefile, Makefile:
+
+       [no log message]
+       
+1999-12-26 21:45  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1999-12-26 21:42  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1999-12-26 21:34  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1999-12-26 21:19  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1999-12-26 18:35  jesse
+
+       * Makefile:
+
+       work on the makefile
+       
+1999-12-26 17:42  jesse
+
+       * lib/RT/User.pm:
+
+       more fixes for the new Handle object
+       
+1999-12-26 17:03  jesse
+
+       * bin/rtmux.pl, lib/RT/ACL.pm, lib/RT/Areas.pm, lib/RT/Queues.pm,
+       lib/RT/Tickets.pm, lib/RT/Transactions.pm, lib/RT/Users.pm:
+
+       more work on making everything use DBIx::Handle
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Ticket.pm:
+
+       file Ticket.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Queue.pm:
+
+       file Queue.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Transaction.pm:
+
+       file Transaction.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Notification.pm:
+
+       file Notification.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/ACE.pm:
+
+       file ACE.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Areas.pm:
+
+       file Areas.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/ACL.pm:
+
+       file ACL.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Users.pm:
+
+       file Users.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Database.pm:
+
+       file Database.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Tickets.pm:
+
+       file Tickets.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Queues.pm:
+
+       file Queues.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Transactions.pm:
+
+       file Transactions.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Area.pm:
+
+       file Area.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/: ACE.pm, ACL.pm, Area.pm, Areas.pm, Database.pm,
+       Notification.pm, Queue.pm, Queues.pm, Record.pm, Ticket.pm,
+       Tickets.pm, Transaction.pm, Transactions.pm, User.pm, Users.pm:
+
+       moving things to a more sane directory
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/Record.pm:
+
+       file Record.pm was initially added on branch rt-1-1.
+       
+1999-12-26 15:58  jesse
+
+       * lib/RT/User.pm:
+
+       file User.pm was initially added on branch rt-1-1.
+       
+1999-12-23 01:05  jesse
+
+       * Makefile, NEWS:
+
+       23 Dec 1999
+       -----------
+       
+       * Enabled a status = unresolved option for the web ui. thanks to
+         brandon allbery <allbery@ece.cmu.edu> and Marion Hakanson <hakanson@cse.ogi.edu>
+       
+       * Made most of the permissions and directory changes from Marion
+         Hakanson <hakanson@cse.ogi.edu> generally cleaned things up. but DID not
+         include the changes to the directory creation, file copying and permission
+         fixing code to enable RT_VAR_DIR
+       
+       * Made the web ui use $MESSAGE_FONT when putting up the compose window.
+           Marion Hakanson <hakanson@cse.ogi.edu>
+       
+       * Genericised the templates to not mention the mythical "systems group"
+       
+1999-12-19 01:54  jesse
+
+       * Makefile, etc/schema.mysql:
+
+       work on ticket, transaction.
+       schema cleanup
+       
+1999-12-18 13:25  jesse
+
+       * etc/schema:
+
+       removed obsolete schema
+       
+1999-12-17 01:28  jesse
+
+       * Makefile:
+
+       set version
+       
+1999-12-17 01:27  jesse
+
+       * etc/: config.pm, schema.mysql:
+
+       more cleanup. we're getting there. just another zillion hours or so
+       
+1999-12-15 23:56  jesse
+
+       * docs/: FAQ, FAQ.html:
+
+       faq updates committed
+       
+1999-12-15 23:26  jesse
+
+       * README:
+
+       updates to tobix' additions
+       
+1999-12-15 18:32  tobiasb
+
+       * README:
+
+       duh
+       
+1999-12-15 18:04  tobiasb
+
+       * README:
+
+       Some few pitfalls mentionated
+       
+1999-12-09 00:38  jesse
+
+       * etc/schema.mysql:
+
+       work on Ticket and Transaction.
+       we're getting there.
+       
+1999-12-08 03:40  tobiasb
+
+       * etc/suidrt.c:
+
+       bugfixes
+       
+1999-12-06 21:41  jesse
+
+       * Makefile, NEWS:
+
+       [no log message]
+       
+1999-12-06 06:27  tobiasb
+
+       * Makefile:
+
+       insignificant output fix
+       
+1999-12-04 03:30  jesse
+
+       * NEWS, etc/schema.mysql:
+
+       [no log message]
+       
+1999-12-04 01:46  jesse
+
+       * NEWS:
+
+       Fixed tobix-induced bug in lib/rt/database/manipulate.pm
+       that'll teach him to make untested fixes to the stable branch.
+       
+1999-12-02 13:18  tobiasb
+
+       * etc/schema:
+
+       I'm unfortunately not even half on the way in my merging. I'm in a hurry, some small notes:
+       
+       1.1:
+       - work on dependencies done
+       - work on mail distribution done.
+       - requires sub dist_list
+       - requires sub open_parents
+       - requires sub add_link
+       
+       Dependencies & linking in general.
+       
+       Mail distribution. The distribution list is set by a &dist_list
+       sub. The transaction mail is never sent out for comments and
+       correspondence, but people who subscribe all transactions will get the
+       comments and correspondence.
+       
+1999-12-02 03:10  tobiasb
+
+       * etc/mysql.acl:
+
+       This file is renamed
+       
+1999-12-02 03:05  tobiasb
+
+       * etc/: schema.Pg, schema.mysql:
+
+       This file was fucked up, I don't know why. I'll just truncate it for now
+       
+1999-12-02 02:56  jesse
+
+       * Makefile:
+
+       bumped version
+       
+1999-12-02 02:54  tobiasb
+
+       * etc/schema:
+
+       Added links (dependency + knowledge db)
+       
+1999-12-02 01:42  jesse
+
+       * Makefile:
+
+       some makefile cleanup
+       
+1999-12-01 23:16  jesse
+
+       * NEWS:
+
+       [no log message]
+       
+1999-12-01 23:10  jesse
+
+       * bin/rtmux.pl, etc/schema.mysql:
+
+       mots of work on the new backend.
+       cliadmin is mostly up to date.
+       no, i haven't run any of the code :)
+       
+1999-11-30 21:52  jesse
+
+       * etc/: acl.Pg, schema.mysql:
+
+       [no log message]
+       
+1999-11-23 17:21  jesse
+
+       * docs/design_docs/CARS:
+
+       file CARS was initially added on branch rt-1-1.
+       
+1999-11-23 17:21  jesse
+
+       * README, docs/design_docs/CARS, etc/schema.mysql:
+
+       schema hacking for rt-1-1 features.
+       
+1999-11-18 21:40  jesse
+
+       * Makefile, Makefile, Makefile:
+
+       [no log message]
+       
+1999-11-17 17:36  tobiasb
+
+       * NEWS:
+
+       merged in changes from 1.0
+       
+1999-11-16 23:09  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1999-11-08 20:54  jesse
+
+       * NEWS:
+
+       fixed a longstanding bug in cli/query
+       
+1999-11-08 18:02  jesse
+
+       * Makefile, NEWS, etc/config.pm:
+
+       [no log message]
+       
+1999-11-05 03:50  tobiasb
+
+       * Makefile, NEWS, README, bin/initacls.mysql, bin/initdb.mysql,
+       etc/acl.mysql, etc/config.pm:
+
+       patch from khamer integrated - no testing done, however
+       
+1999-10-22 14:09  tobiasb
+
+       * etc/schema:
+
+       merged 1.1-changes
+       
+1999-10-22 12:14  tobiasb
+
+       * NEWS, etc/mysql.acl, etc/schema:
+
+       Merged in changes from 1.0. I haven't checked that things works quite well, anyway.
+       
+1999-10-21 16:21  jesse
+
+       * Makefile, NEWS:
+
+       changes to date_diff, some of the oldest code in RT
+       
+1999-10-21 07:07  tobiasb
+
+       * docs/actions.txt, docs/admin.txt, docs/attributes.txt,
+       docs/outline.txt, docs/rt_users_guide.html, etc/config.pm:
+
+       merged from 1.0
+       
+1999-10-20 23:16  jesse
+
+       * NEWS:
+
+       [no log message]
+       
+1999-10-20 22:25  jesse
+
+       * NEWS:
+
+       [no log message]
+       
+1999-10-20 21:58  jesse
+
+       * NEWS:
+
+       [no log message]
+       
+1999-10-20 21:43  jesse
+
+       * Makefile, NEWS:
+
+       [no log message]
+       
+1999-10-20 10:33  tobiasb
+
+       * Makefile, NEWS, README, README.91UPGRADE, TODO,
+       bin/initacls.mysql, bin/initdb.mysql, docs/README.docs:
+
+       Merged 1.0-development into 1.1
+       
+1999-10-20 01:23  jesse
+
+       * Makefile, NEWS:
+
+       
+       20 Oct 1999
+       -----------
+       * RT now uses a queue's mail alias when sending mail.
+       
+1999-10-20 00:44  jesse
+
+       * Makefile, NEWS:
+
+       
+       20 Oct 1999
+       -----------
+       
+       * Using the web UI to send correspondence should no longer not
+         sent the correspondence if there are CCs, BCCs or the actor is
+         the same as the requestor. thanks to <douglas@arepa.com> for
+         pointing out the deficiency.
+       
+1999-10-13 19:32  jesse
+
+       * Makefile, TODO:
+
+       Released version 1.0 Updated the TODO file.
+       
+1999-10-06 18:01  jesse
+
+       * Makefile:
+
+       bumped version
+       
+1999-10-06 17:57  jesse
+
+       * Makefile, NEWS, README, docs/README.docs:
+
+       [no log message]
+       
+1999-10-06 17:48  jesse
+
+       * docs/: FAQ, FAQ.html, README.docs, actions.html, actions.txt,
+       admin.html, admin.txt, attributes.html, attributes.txt,
+       outline.html, outline.txt, rt_users_guide.html:
+
+       major documentation updates from mbrader
+       
+1999-10-04 14:22  jesse
+
+       * Makefile, NEWS:
+
+       bumped the version to 1.0.0pre2
+       
+1999-10-01 01:39  jesse
+
+       * Makefile:
+
+       fixed a makefile typo
+       
+1999-10-01 01:37  jesse
+
+       * NEWS, README, README.91UPGRADE:
+
+       updated readme
+       
+1999-10-01 01:07  jesse
+
+       * NEWS:
+
+       [no log message]
+       
+1999-10-01 00:22  jesse
+
+       * NEWS, etc/config.pm:
+
+       
+       30 Sep 1999
+       -----------
+       
+       *  Fixed a bug which caused RT to go crazy when comments were submitted
+          by may without a ticket #.
+       
+       The following changes are from johnl@microware.com
+       
+          1. Directories were not getting created with the correct modes under            /usr/local/rt/transactions.                                                         The umask() takes an octal file mode mask, not a file mode.                                                                                                     In addition I read the comments about $dirmode not working when             doing the mkdir's in content.pm.  I also fixed content.pm to use                $dirmode.  The main problem was the $dirmode was being set to a string          instead of an octal number.
+       
+         Fixes to database.pm
+           1.  The first hunk fixes problem where the call to write_content is             passing $time.  This variable does not exist.   It looked like $time was        supposed to be the current time.  ($time always == 0).  I changed all           occurances of $time to time (IE: time()).                                                                                                                       2.  The first hunk also passes $in_time instead of $time.                                                                                                       3.  The rest of the hunks fix $time and replace them with time().
+       
+       27 Sep 1999
+       -----------
+       Fixed a bug which caused the priority not to get set to the default
+       when requests were created in the webui. Thanks to <Elmar.Knipp@knipp.de>
+       
+1999-09-23 23:16  anoncvs
+
+       * Makefile:
+
+       [no log message]
+       
+1999-09-23 23:09  anoncvs
+
+       * Makefile, Makefile:
+
+       [no log message]
+       
+1999-09-23 23:07  anoncvs
+
+       * Makefile:
+
+       [no log message]
+       
+1999-09-23 23:01  anoncvs
+
+       * Makefile:
+
+       [no log message]
+       
+1999-09-23 22:52  anoncvs
+
+       * Makefile:
+
+       [no log message]
+       
+1999-08-08 22:20  tobiasb
+
+       * etc/schema:
+
+       ups
+       
+1999-08-07 02:27  tobiasb
+
+       * etc/schema:
+
+       in-work
+       
+1999-08-04 02:48  tobiasb
+
+       * Makefile, NEWS:
+
+       Merge
+       
+1999-08-03 22:38  jesse
+
+       * NEWS:
+
+       first cut of -trans
+       
+1999-08-03 22:27  jesse
+
+       * Makefile, NEWS:
+
+       reved the version to .99.9. tiny mail manipulate fix
+       
+1999-08-03 11:31  tobiasb
+
+       * bin/initdb.mysql:
+
+       file initdb.mysql was initially added on branch rt-1-1.
+       
+1999-08-03 11:31  tobiasb
+
+       * bin/initdb.Pg:
+
+       file initdb.Pg was initially added on branch rt-1-1.
+       
+1999-08-03 11:31  tobiasb
+
+       * bin/initacls.mysql:
+
+       file initacls.mysql was initially added on branch rt-1-1.
+       
+1999-08-03 11:31  tobiasb
+
+       * bin/: initacls.Pg, initacls.mysql, initdb.Pg, initdb.mysql:
+
+       Scripts for initing the DB
+       
+1999-08-03 11:31  tobiasb
+
+       * bin/initacls.Pg:
+
+       file initacls.Pg was initially added on branch rt-1-1.
+       
+1999-08-03 10:31  tobiasb
+
+       * etc/schema.mysql:
+
+       file schema.mysql was initially added on branch rt-1-1.
+       
+1999-08-03 10:31  tobiasb
+
+       * etc/schema.Pg:
+
+       file schema.Pg was initially added on branch rt-1-1.
+       
+1999-08-03 10:31  tobiasb
+
+       * etc/acl.mysql:
+
+       file acl.mysql was initially added on branch rt-1-1.
+       
+1999-08-03 10:31  tobiasb
+
+       * etc/acl.Pg:
+
+       file acl.Pg was initially added on branch rt-1-1.
+       
+1999-08-03 10:31  tobiasb
+
+       * Makefile, etc/acl.Pg, etc/acl.mysql, etc/mysql.acl, etc/schema,
+       etc/schema.Pg, etc/schema.mysql:
+
+       More work on the move to a DBMS-independent RT. The Makefile and executables _really_ need some testing and eventually debugging.
+       
+1999-08-03 02:14  tobiasb
+
+       * Makefile, etc/config.pm:
+
+       It seems to me that I've somehow managed to go to DBI...WebRT seems to work locally, but that's the only testing I've done
+       
+1999-08-02 07:07  tobiasb
+
+       * etc/schema:
+
+       in-work
+       
+1999-07-29 02:55  tobiasb
+
+       * etc/schema:
+
+       uuuurghh
+       
+1999-07-28 10:42  tobiasb
+
+       * README:
+
+       One small update on the work to link requests...
+       
+1999-07-27 07:08  tobiasb
+
+       * NEWS:
+
+       1.0-gospel
+       
+1999-07-27 06:48  tobiasb
+
+       * etc/schema:
+
+       asdfjlsadfhjkhdfawhi
+       
+1999-07-26 21:21  jesse
+
+       * Makefile, NEWS:
+
+       fixes to the mail gateway header handling
+       added cli rtq flags to the help
+       
+1999-07-26 07:03  tobiasb
+
+       * NEWS:
+
+       Merge
+       
+1999-07-24 20:43  jesse
+
+       * Makefile:
+
+       bumpted the version
+       
+1999-07-24 20:40  jesse
+
+       * NEWS:
+
+       added a few options to the cli query engine. area limitying and display of due dates.
+       
+1999-07-23 03:20  tobiasb
+
+       * NEWS:
+
+       Some merges
+       
+1999-07-23 02:56  tobiasb
+
+       * etc/schema:
+
+       With generic links
+       
+1999-07-22 23:13  jesse
+
+       * Makefile, NEWS:
+
+       22 Jul 99 (Jesse)
+       -----------------
+       Inital cleanup to the web ticket view. I'd like to significantly
+       compact and simplify that display.  More tables are probably in
+       order.  Additionally, I want to get rid of all those buttons "I
+       changed this." There's just no need for them. we can write logic
+       to do that for us.  Also, I bumped the version to 1.1.1.  I'd like
+       to do this 'linux-kernel-esque' The 1.1.x
+       series will be a development series leading toward 1.2.  Once we
+       get the DB changes in, I'll feel comfortable rolling a 'releaseable'
+       1.1 version for people who like pain.
+       
+1999-07-21 23:47  jesse
+
+       * Makefile, NEWS:
+
+       21 July 1999
+       ------------
+       
+       Fix for [fsck #102] Better checks in is_not_a_requestor. this should fix
+               issues with external users being equated with queue members.
+       
+       Fix for [fsck #114] Comment from bin/rt was accidentaly access controlled.
+       
+       Fix for [fsck #113] Display of requests users can't manipulate should now give
+               ian error
+       
+1999-07-20 23:40  tobiasb
+
+       * etc/schema:
+
+       For a general way of linking WebRT requests to other web-based DBs
+       
+1999-07-20 21:42  tobiasb
+
+       * NEWS:
+
+       [no log message]
+       
+1999-07-20 21:11  tobiasb
+
+       * NEWS:
+
+       Hmf.
+       
+1999-07-20 21:07  tobiasb
+
+       * README, etc/config.pm:
+
+       updated for Internet::Mail
+       
+1999-07-20 21:01  tobiasb
+
+       * Makefile:
+
+       updated for Internet::Mail
+       
+1999-07-20 01:21  jesse
+
+       * NEWS:
+
+       Fix for [fsck #88] WebUI area SELECT bug.
+       
+       Fix for [fsck #90] A small html bug in WebRT
+       
+       Fix for [fsck #92] "Allow non-members to create requests" appeared twice
+               in the WebAdmin
+       
+       Fix for [fsck #94] web ui forms are now 78 chars wide
+       
+1999-07-20 00:09  jesse
+
+       * NEWS:
+
+       19 July 1999
+       ------------
+       Fix for [fsck #104]  We no longer try to reset the user's uid after writing
+               transaction content to the filesystem. This should help out Net/OpenBSD
+               compatibility
+       
+       Removed some legacy support for glimpse searching
+       
+       Fix for [fsck #115] The web ticket list should now wrap reasonably, so it's
+               printable.
+       
+1999-07-19 03:26  tobiasb
+
+       * NEWS:
+
+       some updates
+       
+1999-07-17 13:14  tobiasb
+
+       * NEWS:
+
+       Updated a bit
+       
+1999-07-16 23:42  tobiasb
+
+       * Makefile:
+
+       Version number 1.pre1.1 - what do you think about it?
+       
+1999-07-16 22:49  tobiasb
+
+       * bin/rtmux.pl:
+
+       Filled in a missing comment
+       
+1999-07-16 22:46  tobiasb
+
+       * Makefile:
+
+       Set version to 1.1.0pre1, and changed /opt/rt to /usr/local/rt, I'd
+       daresay the next is more widely preffered.
+       
+1999-07-16 18:36  tobiasb
+
+       * COPYING, Makefile, NEWS, README, README.91UPGRADE, TODO,
+       bin/rtmux.pl, docs/FAQ, docs/FAQ.html, docs/README.docs,
+       docs/actions.html, docs/actions.txt, docs/admin.html,
+       docs/admin.txt, docs/attributes.html, docs/attributes.txt,
+       docs/outline.html, docs/outline.txt, etc/config.pm, etc/mysql.acl,
+       etc/schema, etc/suidrt.c:
+
+       Imported Tobix' current version
+       
+1999-07-08 02:10  jesse
+
+       * Makefile, NEWS, TODO:
+
+       Rolled .99.8
+       
+1999-07-07 15:37  jesse
+
+       * NEWS:
+
+       [no log message]
+       
+1999-07-07 00:45  jesse
+
+       * Makefile, NEWS:
+
+       updated makefile and news.
+       
+1999-07-06 22:58  jesse
+
+       * NEWS:
+
+       6 Jul 99
+       --------
+       Now, if you move a request to a new queue, it won't disown it if the same person can own reqs in the new queue. [fsck #75] (untested)
+       
+       Fixed a problem with cli create not properly handling due dates ([fsck #67])
+       Fixed a duplicate --help entry for bin/rt [fsck #69]
+       Fixed bugs fsck  #68/77: Odness Merging RT requests
+       
+       Transactions from merged requests will now be displayed along with the request id of the request that transaction was originially associated with.
+       
+       Fixed [fsck #74], which was reported by charlie brady:
+          Messages are sent out from the mail interface with content first, then
+          "--- Headers Follow ---", then the headers. In our vanilla sendmail setup,
+          before the headers is a UUCP style deliver notification "From blah Tue Jul
+          6 18:13:01 1999" line. This is interpreted by many mail agents as a
+          message delimiter, so that what is seen in the mail agent is not one, but
+          two messages. This is easily fixed by using the conventional quoting
+          mechanism...
+       Fixed [fsck #71] WebRT: "User" should be "Requestor"
+       
+1999-07-06 21:35  jesse
+
+       * NEWS:
+
+       resovled #71, #74. worked on merge functionality
+       
+1999-06-29 17:39  jesse
+
+       * Makefile, NEWS:
+
+       [no log message]
+       
+1999-06-29 17:20  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1999-06-24 00:52  jesse
+
+       * Makefile, NEWS, README, TODO:
+
+       lots of touchups.
+       
+1999-06-23 23:29  jesse
+
+       * NEWS:
+
+       moved to CGI.pm's form parser.
+       
+1999-06-17 01:49  jesse
+
+       * NEWS:
+
+       edited news
+       
+1999-06-16 23:21  jesse
+
+       * Makefile, NEWS:
+
+       Fixed an auth bug.
+       
+1999-06-15 02:13  jesse
+
+       * Makefile:
+
+       upped the version to .99.8pre5
+       
+1999-06-15 02:09  jesse
+
+       * Makefile, NEWS:
+
+       lots of misc changes. see the diff to the news file
+       
+1999-05-12 20:37  jesse
+
+       * Makefile, NEWS:
+
+       cookies hacked to deal with the fact that netscape can't handle path=/
+       
+1999-05-12 01:57  jesse
+
+       * Makefile:
+
+       bumped the version number to .99.8pre3
+       
+1999-05-12 01:56  jesse
+
+       * NEWS:
+
+       Made error messages on the web admin interface more prominent.
+       
+       All the following changes are from tobix:
+       
+               Correspondence from RT now properly reflect the name of the sender
+               in the "from" header.
+       
+               Automated messages now properly have a "Precedence: bulk" header
+       
+               A stupid error with rt::mail_alias has been fixed
+       
+               Tobix' patch removes headers from all correspondence.  I'm relatively
+               afraid of this change, so I'm going to comment it out for now.
+       
+               rtadmin (the commandline has a new tool) :
+                    -update <passwd> <admin> [<users>]  updates user(s) from the
+                          /etc/passwd file. If no users are specified, ALL
+                          of /etc/passwd will be processed.
+       
+1999-05-11 16:46  jesse
+
+       * Makefile, NEWS, README:
+
+       mail loop fix from toby
+       
+1999-05-09 20:35  jesse
+
+       * Makefile:
+
+       bumped version numbers
+       
+1999-05-09 20:35  jesse
+
+       * Makefile, NEWS, README:
+
+       updated makefile and news
+       
+1999-05-09 20:17  jesse
+
+       * bin/rtmux.pl:
+
+       modifications to cookies support to md5 hash the password.
+       modification to allow non-nph web ui
+       
+1999-05-05 02:39  jesse
+
+       * NEWS:
+
+       fixed some cookies/frames problems
+       
+1999-05-05 02:10  jesse
+
+       * Makefile, NEWS:
+
+       fixed bugs in web auth from charlie brady
+       
+1999-05-03 21:44  jesse
+
+       * Makefile:
+
+       bumped the version #
+       
+1999-04-28 16:03  jesse
+
+       * Makefile:
+
+       bumped version
+       
+1999-04-28 16:03  jesse
+
+       * NEWS:
+
+       updated content.pm to spit errors.
+       
+1999-04-27 02:58  jesse
+
+       * Makefile:
+
+       downed the makefile version
+       
+1999-04-27 02:57  jesse
+
+       * Makefile:
+
+       fixed a makefile bug
+       
+1999-04-26 21:57  jesse
+
+       * NEWS, README:
+
+       added configuration info about mod_auth_mysql
+       
+1999-04-26 21:45  jesse
+
+       * Makefile, etc/config.pm:
+
+       Updated authetication stuff with patches from ingo. updated them further.
+       Added the ability to configure whether external authentication is done for webrt
+       added the ability to turn off ie compatibility mode
+       
+1999-04-23 19:31  jesse
+
+       * Makefile, NEWS, README, TODO:
+
+       started to update the README and Makefile
+       
+1999-04-13 04:00  jesse
+
+       * Makefile:
+
+       fixed totally fuxored makefile
+       
+1999-04-13 03:28  jesse
+
+       * README:
+
+       noted that there's a dependency on gnu make
+       
+1999-04-13 03:07  jesse
+
+       * Makefile:
+
+       bumped version
+       
+1999-04-13 03:06  jesse
+
+       * NEWS:
+
+       updated news
+       
+1999-04-08 04:36  jesse
+
+       * docs/rt.gif:
+
+       added the gif to the docs
+       
+1999-04-08 04:35  jesse
+
+       * NEWS:
+
+       fixes for new mysql modules
+       
+1999-04-07 23:57  jesse
+
+       * NEWS:
+
+       updates
+       
+1999-04-07 02:14  jesse
+
+       * NEWS:
+
+       [no log message]
+       
+1999-04-07 02:09  jesse
+
+       * Makefile, README:
+
+       fixes for cookie auth from charlie brady
+       
+1999-04-06 06:30  jesse
+
+       * Makefile, NEWS, README:
+
+       misc changes to support cookie authentication
+       
+1999-04-03 04:57  jesse
+
+       * NEWS, docs/FAQ, docs/FAQ.html, docs/README.docs,
+       docs/actions.html, docs/actions.txt, docs/admin.html,
+       docs/admin.txt, docs/attributes.html, docs/attributes.txt,
+       docs/outline.html, docs/outline.txt:
+
+       major update of the docs from adam.
+       
+1999-04-03 04:49  jesse
+
+       * Makefile:
+
+       makefile hackery to ease installs slightly.
+       
+1999-03-21 20:24  jesse
+
+       * Makefile:
+
+       updated makefile
+       
+1999-03-21 20:22  jesse
+
+       * NEWS, README:
+
+       mail changes
+       
+1999-03-08 14:50  jesse
+
+       * NEWS:
+
+       turned off the troll
+       
+1999-03-08 14:02  jesse
+
+       * Makefile:
+
+       fixed makefile bogosity for ACLs
+       
+1999-03-04 01:50  jesse
+
+       * Makefile, NEWS:
+
+       fixed a subject line parsing error.
+       
+1999-02-26 22:29  jesse
+
+       * NEWS:
+
+       added a check to make sure you don't merge ar equest into a non-existent other request
+       
+1999-02-26 21:43  jesse
+
+       * README.91UPGRADE, README.FIRST, TODO:
+
+       moved readme.fdirst to readmne.91upgrade
+       
+1999-02-26 19:59  jesse
+
+       * Makefile, NEWS:
+
+       updated makefile: added upgrade-noclobber
+       
+1999-02-24 19:11  jesse
+
+       * NEWS:
+
+       [no log message]
+       
+1999-02-23 14:41  jesse
+
+       * NEWS:
+
+       updated bnews
+       
+1999-02-23 03:20  jesse
+
+       * docs/: actions.txt, admin.txt, attributes.txt:
+
+       added updates for adam for the docs.
+       
+       updated the news
+       
+1999-02-23 03:20  jesse
+
+       * NEWS:
+
+       updated the news
+       
+1999-02-21 01:56  jesse
+
+       * Makefile:
+
+       fixed the .4
+       
+1999-02-21 01:53  jesse
+
+       * Makefile:
+
+       fixed make pre
+       
+1999-02-20 23:27  jesse
+
+       * Makefile:
+
+       addded back predist
+       
+1999-02-20 16:36  jesse
+
+       * NEWS:
+
+       edited the news to reflect changes
+       
+1999-02-20 14:56  jesse
+
+       * Makefile, NEWS:
+
+       bumped the version to .99.4 for testing.
+       
+1999-02-20 14:48  jesse
+
+       * etc/mysql.acl:
+
+       updated acls so they'll work with newer versions of mysql 3.22
+       
+1999-01-20 02:21  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1999-01-20 02:13  jesse
+
+       * Makefile:
+
+       On another note, here is a fix for the "HTML turd" reported by Benji
+       Cline in lib/rt/ui/web/forms.pm:
+       ==========                                                                      diff -r0.99.2 forms.pm
+       87c87                                                                           <               $u = 0;
+       ---                                                                             >               $u = 1;
+       91c91                                                                           <                       $u = 1;                                                 ---                                                                             >                       $u = 0;
+       ==========
+       
+       With this patch applied, v0.99.2 (0.99.3?)                                      appears to be as solid as v0.9.18, if not more
+       so.
+       
+       Upgraded the version to .99.3
+       
+1999-01-16 20:44  jesse
+
+       * NEWS:
+
+       [no log message]
+       
+1999-01-16 20:39  jesse
+
+       * Makefile:
+
+       fixed a web acls bug. incremented the version.
+       
+1999-01-16 01:22  jesse
+
+       * Makefile:
+
+       bumped version to .99.1
+       
+1999-01-16 01:20  jesse
+
+       * Makefile, NEWS:
+
+       fixed some web admin sillyness..acls work better.
+       we no longer dump back to the main menu quite as gratuitously.
+       acls are no longer broken on the CLI either.
+       
+1998-12-16 01:31  jesse
+
+       * Makefile:
+
+       fixed the following things:
+       
+       From owner-rt-users@horked.fsck.com  Tue Nov 24 02:24:03 1998
+       Return-Path: <owner-rt-users@lists.fsck.com>
+       Received: (from majordomo@localhost)
+               by horked.fsck.com (8.8.7/8.8.7) id CAA11181
+               for rt-users-outgoing; Tue, 24 Nov 1998 02:24:01 -0500
+       X-Authentication-Warning: horked.fsck.com: majordomo set sender to owner-rt-users@lists.fsck.com using -f
+       Received: from modgud.nordicdms.com (h21-168-107.nordicdms.com [207.21.168.107] (may be forged))
+               by horked.fsck.com (8.8.7/8.8.7) with SMTP id CAA11177
+               for <rt-users@lists.fsck.com>; Tue, 24 Nov 1998 02:23:58 -0500
+       Received: (qmail 6211 invoked by alias); 24 Nov 1998 07:25:59 -0000
+       Message-ID: <19981124072559.6209.qmail@modgud.nordicdms.com>
+       Received: (qmail 6197 invoked from network); 24 Nov 1998 07:25:59 -0000
+       Received: from mail-ftp.nordicdms.com (HELO mail-ftp) (207.21.168.100)
+         by mail.nordicdms.com with SMTP; 24 Nov 1998 07:25:59 -0000
+       From: "Dave Walton" <walton@nordicdms.com>
+       Organization: Nordic Entertainment Worldwide
+       To: Jesse <jrvincent@wesleyan.edu>, rt-users@lists.fsck.com
+       Date: Mon, 23 Nov 1998 23:25:58 -0800
+       MIME-Version: 1.0
+       Content-type: text/plain; charset=US-ASCII
+       Content-transfer-encoding: 7BIT
+       Subject: Two bugfixes
+       Reply-to: walton@nordicdms.com
+       In-reply-to: <19981013013318.H18644@horked.fsck.com>
+       References: <19981013042506.9798.qmail@modgud.nordicdms.com>; from Dave Walton on Mon, Oct 12, 1998 at 09:25:06PM -0700
+       X-mailer: Pegasus Mail for Win32 (v3.01d)
+       Sender: owner-rt-users@lists.fsck.com
+       Precedence: bulk
+       
+       1.  A backwards search was causing lib/rt/ui/web/support.pm to
+       crash when data that looks like an invalid regex is present in the
+       message headers.
+       ------------------------------------------------------------
+       # diff support.old.pm support.pm
+       44c44
+       <               ($headers_ignore !~ /$field/i)) {
+       ---
+       >               ($field !~ /$headers_ignore/i)) {
+       ------------------------------------------------------------
+       
+       2.  HTML buglet in lib/rt/ui/web/manipulate.pm.
+       ------------------------------------------------------------
+       # diff manipulate.old.pm manipulate.pm
+       894c894
+       < <font color=\"\$fg_color\">
+       ---
+       > <font color=\"$fg_color\">
+       ------------------------------------------------------------
+       
+       Dave
+       
+       ----------------------------------------------------------------------
+       Dave Walton
+       Webmaster, Postmaster                   Nordic Entertainment Worldwide
+       walton@nordicdms.com                          http://www.nordicdms.com
+       ----------------------------------------------------------------------
+       
+       From benji@hnt.com  Fri Nov 20 14:28:44 1998
+       Return-Path: <benji@hnt.com>
+       Received: from horked.fsck.com (jesse@localhost [127.0.0.1])
+               by horked.fsck.com (8.8.7/8.8.7) with ESMTP id OAA04739
+               for <jesse@localhost>; Fri, 20 Nov 1998 14:28:44 -0500
+       Received: from mail.wesleyan.edu
+               by horked.fsck.com (fetchmail-4.3.2 POP3 run by jrvincent)
+               for <jesse@localhost> (single-drop); Fri Nov 20 14:28:44 1998
+       Received: from columbus.hnt.com (columbus.hnt.com [208.221.11.10]) by mail.wesleyan.edu (8.8.6/8.7.3) with ESMTP id OAA18408 for <jrvincent@wesleyan.edu>; Fri, 20 Nov 1998 14:30:34 -0500 (EST)
+       Received: from peppermint.hnt.com (peppermint.hnt.com [208.221.11.51])
+               by columbus.hnt.com (8.9.1/8.9.1/HnT-980729) with ESMTP id OAA26134
+               for <jrvincent@wesleyan.edu>; Fri, 20 Nov 1998 14:30:32 -0500 (EST)
+       Received: from localhost by peppermint.hnt.com (8.9.1/8.9.1/HnT-nullclient-980724) with ESMTP id OAA02733
+               for <jrvincent@wesleyan.edu>; Fri, 20 Nov 1998 14:30:32 -0500 (EST)
+       X-Authentication-Warning: peppermint.hnt.com: benji owned process doing -bs
+       Date: Fri, 20 Nov 1998 14:30:31 -0500 (EST)
+       From: Benjamin Cline <benji@hnt.com>
+       To: Jesse <jrvincent@mail.wesleyan.edu>
+       Subject: Re: 0.9.20 is out
+       In-Reply-To: <19981120005718.F28841@horked.fsck.com>
+       Message-ID: <Pine.GSO.4.05.9811201427100.2438-100000@peppermint.hnt.com>
+       MIME-Version: 1.0
+       Content-Type: TEXT/PLAIN; charset=US-ASCII
+       X-UIDL: 4f1695a69f7cdbefcfce9198121fef95
+       
+       I think I've found a buglet in 0.9.20, I had to add an extra curly bracket
+       ("}") to the end of lib/rt/database/admin.pm (just before the "1;") to get
+       it to work right.
+       
+               benji
+       
+       P.S. Sorry if that's not the most coherent bug report/fix, I'm afraid I'm
+       not much of a perl hacker.
+       
+       On Fri, 20 Nov 1998, Jesse wrote:
+       
+       > ftp://horked.fsck.com/pub/rt/devel/rt-0.9.20.tar.gz
+       >
+       > 19 Nov 98
+       > ---------
+       >
+       > Incorportated patches from Dave Walton to fix a typo, replace an
+       > accidentally
+       > blown away library and add a "last acted" column to the web ui.
+       > incremented version to .9.20
+       >
+       >
+       >       jesse
+       >
+       > --
+       > jesse reed vincent -- jrvincent@wesleyan.edu -- jesse@fsck.com
+       > pgp keyprint:  50 41 9C 03 D0 BC BC C8 2C B9 77 26 6F E1 EB 91
+       > --------------------------------------------------------------
+       > They'll take my private key when they pry it from my cold dead fingers!
+       >
+       
+       --
+       Benjamin R. Cline       Harrison & Troxell, Inc.         benji@hnt.com
+                            Quis Custodiet Ipsos Custodes?
+       
+       From adam@baz.org  Fri Nov 13 17:07:09 1998
+       Return-Path: <adam@baz.org>
+       Received: from horked.fsck.com (jesse@localhost [127.0.0.1])
+               by horked.fsck.com (8.8.7/8.8.7) with ESMTP id RAA10943
+               for <jesse@localhost>; Fri, 13 Nov 1998 17:07:09 -0500
+       Received: from mail.wesleyan.edu
+               by horked.fsck.com (fetchmail-4.3.2 POP3 run by jrvincent)
+               for <jesse@localhost> (single-drop); Fri Nov 13 17:07:09 1998
+       Received: from impei.baz.org (adam@impei.baz.org [139.167.64.229]) by mail.wesleyan.edu (8.8.6/8.7.3) with ESMTP id RAA08425 for <jrvincent@wesleyan.edu>; Fri, 13 Nov 1998 17:08:38 -0500 (EST)
+       Received: (from adam@localhost)
+               by impei.baz.org (8.8.7/8.8.8) id RAA18674
+               for jrvincent@wesleyan.edu; Fri, 13 Nov 1998 17:08:38 -0500
+       Message-ID: <19981113170837.A18668@baz.org>
+       Date: Fri, 13 Nov 1998 17:08:37 -0500
+       From: secret agent man <adam@baz.org>
+       To: "J.R.Ewing" <jrvincent@mail.wesleyan.edu>
+       Subject: RT bug, mebbe?
+       Mime-Version: 1.0
+       X-Mailer: Mutt 0.93.2i
+       Content-Type: text/plain; charset=us-ascii
+       X-UIDL: 5563064ca159a5eee88e16843932a45f
+       
+       <jailbait> q: If you leave the field blank when clicking on "create new
+       (queue|user) named", it goes to the create screen with an empty field that
+       can't be filled. It should either be editable there or it should reject
+       you.
+       
+       A
+       
+       --
+       Everything I needed to know about life I learned       <adam@baz.org>
+       from killing smarter people and eating their brains.    adam hirsch
+       
+1998-11-20 00:45  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1998-11-20 00:28  jesse
+
+       * Makefile, NEWS:
+
+       19 Nov 98
+       ---------
+       
+       Incorportated patches from Dave Walton to fix a typo, replace an accidentally
+       blown away library and add a "last acted" column to the web ui.
+       incremented version to .9.20
+       
+1998-11-18 02:26  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1998-10-13 03:37  jesse
+
+       * Makefile, README, etc/mysql.acl:
+
+       shiny:% cat >> changes                                          ~ 10:53PM:tt
+       2.  Made comments and replies update date_acted.
+       
+               -- Dave Walton
+       
+       Ok.. I know this is being picky, but, in rt/lib/ui/web/support.pm, the
+       subroutine "content_header", at line 290, should have:
+           print "<head><title>WebRT</title></head>\n";
+       
+       Otherwise, when running in frames, you never get a title on the page. :(
+       
+       No biggie...
+       
+       -Rich
+       
+       Oh, one other.
+       
+       8.  etc/mysql.acl, line 5
+       The RT_MYSQL_HOST entry on that line doesn't make any sense
+       when MySQL is on a different host than RT.  That should be
+       RT_HOST, or some such thing.
+       
+       Dave
+       
+       After just upgrading to Apache 1.3.0 from 1.2.5, I figured I would make it
+       known that, in the file <apache_src_dir>/src/main/util_script.c, you have
+       to define SECURITY_HOLE_PASS_AUTHORIZATION to allow for RT's authorization
+       to work properly.
+       -Rich
+       
+       On Tue, Oct 06, 1998 at 09:19:07PM -0700, Dave Walton wrote:
+       > I just discovered that if there is an error in add_correspondence, the
+       > mail interface discards the incoming mail without comment.  To
+       > correct this, I took the following steps:
+       >
+       In mail.pm, there's a space before :
+       
+       From: $rt::mail_alias
+       
+       This causes problems in some mail clients, putting the From: line on the
+       same line as the subject.  If the spaces are removed this is fixed.
+       
+       Regards,
+       From: "Andrew Foster" <adf@fl.net.au>
+       
+1998-09-11 01:15  jesse
+
+       * docs/README.docs:
+
+       updated lists for rt docs.
+       
+1998-09-08 03:23  jesse
+
+       * NEWS:
+
+       updated news\18
+       
+1998-09-08 03:18  jesse
+
+       * Makefile, NEWS, README, etc/config.pm:
+
+       Final bug fixes for .9.18
+       
+1998-08-09 17:43  jesse
+
+       * NEWS, README, TODO:
+
+       All kinds of crazy updates for .9.18. Mostly from serge zhuk
+       
+1998-08-04 03:09  jesse
+
+       * NEWS:
+
+       first round of updates from serge
+       
+1998-06-26 17:07  jesse
+
+       * Makefile, NEWS:
+
+       modified makefile and news
+       
+1998-06-26 16:34  jesse
+
+       * docs/: README.docs, actions.txt, admin.txt, attributes.txt,
+       outline.txt:
+
+       Added documentation from <adam@apocalypse.org>
+       
+1998-06-25 17:06  jesse
+
+       * Makefile, README, etc/config.pm:
+
+       removed refs to glimpse
+       
+1998-06-25 15:46  jesse
+
+       * Makefile:
+
+       dist fixes
+       
+1998-06-25 15:44  jesse
+
+       * Makefile:
+
+       dist fixes
+       
+1998-06-25 14:09  jesse
+
+       * Makefile:
+
+       edited dist. maker
+       
+1998-05-21 22:40  jesse
+
+       * Makefile:
+
+       added httpd.conf configurator for cern
+       
+1998-04-27 20:17  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1998-04-20 14:53  jesse
+
+       * Makefile:
+
+       bumped version to .9.14.
+       
+1998-04-20 14:53  jesse
+
+       * Makefile:
+
+       bumped version to .9.13.1
+       
+1998-04-19 03:14  jesse
+
+       * Makefile:
+
+       updated version number.
+       now explicitly create rt-etc-dir
+       
+1998-04-16 21:40  jesse
+
+       * bin/rtmux.pl:
+
+       removed a spurious addition to the lib path from the mux
+       abstracted a bunch of stuff out of the mux and throughout the makefile.
+       like program names.
+       
+1998-04-16 21:39  jesse
+
+       * Makefile:
+
+       abstracted a bunch of stuff out of the mux and throughout the makefile.
+       like program names.
+       
+1998-04-16 19:47  jesse
+
+       * COPYING, Makefile, NEWS, README, README.FIRST, TODO:
+
+       commiting updates
+       
+1998-04-08 13:35  jesse
+
+       * etc/config.pm:
+
+       fixed typos in config.pm that prevented mail from working with .9.11
+       bumped version to .12
+       
+1998-04-08 13:34  jesse
+
+       * Makefile:
+
+       bumped version to .12
+       
+1998-04-02 21:30  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1998-04-02 21:27  jesse
+
+       * Makefile:
+
+       fixed make dist
+       
+1998-04-02 15:36  jesse
+
+       * Makefile, Makefile:
+
+       [no log message]
+       
+1998-04-02 15:29  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1998-04-02 15:25  jesse
+
+       * etc/config.pm:
+
+       split mail program and mail program flags
+       hopefully fixed make dist
+       
+1998-04-02 15:25  jesse
+
+       * Makefile:
+
+       hopefully fixed make dist
+       
+1998-04-02 11:02  jesse
+
+       * Makefile, NEWS:
+
+       updated news
+       
+1998-04-02 10:55  jesse
+
+       * Makefile:
+
+       [no log message]
+       
+1998-04-02 10:31  jesse
+
+       * etc/config.pm:
+
+       reshuffled files that the user won't be changing out of etc and into lib
+       this includes generic templates and images for the web ui
+       updated src and makefiles accordingly.
+       verbosifoed part of config.pm
+       
+1998-04-02 10:31  jesse
+
+       * Makefile:
+
+       reshuffled files that the user won't be changing out of etc and into lib
+       this includes generic templates and images for the web ui
+       updated src and makefiles accordingly.
+       
+1998-04-02 10:20  jesse
+
+       * Makefile:
+
+       built more auto-dist stuff
+       
+1998-04-02 03:22  jesse
+
+       * README:
+
+       added apache patch to readme
+       
+1998-04-02 03:13  jesse
+
+       * Makefile, README, TODO, etc/config.pm:
+
+       updated makefile and readme and todo
+       added toggle for mysql 3.21
+       moved sendmail flags to makefile for compat w 8.6
+       
+1998-02-08 21:12  jesse
+
+       * Makefile:
+
+       make upgrade now calls mux-install
+       
+1998-01-30 22:31  jesse
+
+       * Makefile, README, README.FIRST:
+
+       added bits to upgrade from .9.1
+       made make upgrade upgrade the conf file
+       
+1998-01-27 01:56  jesse
+
+       * NEWS:
+
+       updated news
+       
+1998-01-27 01:48  jesse
+
+       * Makefile:
+
+       started a make dist
+       
+1998-01-26 17:59  jesse
+
+       * Makefile, README, README.FIRST:
+
+       updated readme
+       
+1998-01-21 18:37  jesse
+
+       * Makefile:
+
+       fixes to makefile to allow for proper specification of database as local.
+       added "you are logged in as..." to web/support...
+       changed sendmail options in mail.pm/
+       
+1998-01-14 23:49  jesse
+
+       * Makefile, etc/mysql.acl:
+
+       incremented version number
+       
+1998-01-09 14:48  jesse
+
+       * bin/rtmux.pl:
+
+       added taint safeness for $ENV{'ENV}
+       updated makefile version number
+       
+1998-01-09 14:47  jesse
+
+       * Makefile:
+
+       updated makefile version number
+       
+1998-01-08 16:17  jesse
+
+       * README.FIRST:
+
+       ipdated readme.first.
+       
+1998-01-08 14:53  jesse
+
+       * etc/suidrt.c:
+
+       made suidrt.c work
+       updated the mux
+       
+1998-01-08 14:53  jesse
+
+       * bin/rtmux.pl:
+
+       updated the mux
+       
+1998-01-08 13:10  jesse
+
+       * Makefile:
+
+       more fixes to make suid wrapper stuff work.
+       
+1998-01-08 12:51  jesse
+
+       * bin/rtmux.pl:
+
+       made rtmux attempt to work with the suid wrapper
+       
+1998-01-08 12:48  jesse
+
+       * etc/suidrt.c:
+
+       added back suidrt.c. blech.
+       
+1998-01-08 12:44  jesse
+
+       * Makefile, bin/rtmux.pl:
+
+       brought back suidrt.c
+       bleck.
+       
+1998-01-07 18:06  jesse
+
+       * Makefile:
+
+       added a sane mode for $(RT_PATH)
+       
+1998-01-07 00:37  jesse
+
+       * Makefile, README.FIRST:
+
+       added basic old upgrade instructions.
+       added info to README.FIRST about upgrading.
+       
+1998-01-07 00:31  jesse
+
+       * bin/rtmux.pl:
+
+       made mailing work.
+       
+1998-01-06 10:27  jesse
+
+       * bin/rtmux.pl:
+
+       made the mux require ui::mail before it tried to invoke it.
+       
+1998-01-04 02:26  jesse
+
+       * README:
+
+       told users to read readme.first.
+       
+1998-01-04 02:25  jesse
+
+       * README, README.FIRST:
+
+       added readme.first
+       
+1998-01-04 02:12  jesse
+
+       * Makefile:
+
+       updated webadmin interface. fixed some logout problems. fixed an html bug in web-manip.
+       
+1998-01-02 01:14  jesse
+
+       * Makefile, bin/rtmux.pl:
+
+       fixed more problems w/ making cliadmin run
+       merged mux-install and mux-links into one section of makefile
+       
+1998-01-02 00:59  jesse
+
+       * Makefile, NEWS, README, TODO, bin/rtmux.pl, etc/config.pm:
+
+       fixed typo in Makefile: install-libs -> libs-install
+       
+1998-01-01 03:07  jesse
+
+       * Makefile, bin/rtmux.pl:
+
+       commented makefile better
+       added more stuff for taint checks to rtmux.pl
+       fixed a non-frames web bug in FormComment
+       
+1997-12-31 01:55  jesse
+
+       * Makefile, bin/rtmux.pl, etc/config.pm:
+
+       lots of cleanup to get web interface running.
+       beginnigs of taint-safe scripts.
+       
+1997-12-30 23:49  jesse
+
+       * Makefile, bin/rtmux.pl:
+
+       cleanup in cli admin.pm, cli query.pm, the mux, the makefile
+       database routines now assign an effective serial # to a request when you get
+       the fact's serial number
+       
+1997-12-30 22:56  jesse
+
+       * Makefile, README, bin/rtmux.pl:
+
+       remodded rtmux to use correct vars for command line arguments.
+       moved RT_VERSION to makefile.
+       updated README for new install procedure
+       
+1997-12-30 01:09  jesse
+
+       * Makefile, etc/config.pm, etc/mysql.acl:
+
+       fixed typo in web/admin.pm
+       set up config.pm mysql.acl and rtmux.pl to get themselves parsed from the makefile.
+       removed last vestiges of C
+       
+1997-12-28 18:59  jesse
+
+       * Makefile, etc/schema:
+
+       removed suidrt for a preparation to move to a c-free system
+       added provisions for token-parsing to makefile
+       
+1997-12-28 03:10  jesse
+
+       * etc/schema:
+
+       added tags to schema
+       
+1997-12-28 02:48  jesse
+
+       * Makefile:
+
+       started to modify toplevel makefile
+       
+1997-12-19 03:04  jesse
+
+       * lib/rtmux.pl:
+
+       moved the executable rtmux.pl to bin, where it belongs.
+       made sure bin/cgi gets created on checkout
+       
+1997-12-19 03:03  jesse
+
+       * bin/rtmux.pl:
+
+       moved the executable rtmux.pl to bin, where it belongs.
+       
+1997-12-18 19:05  jesse
+
+       * lib/rtmux.pl:
+
+       worked on the mux
+       
+1997-12-09 15:14  jesse
+
+       * TODO:
+
+       added things for v1.0 to the todo list
+       
+1997-12-09 02:03  jesse
+
+       * etc/config.pm:
+
+       broke configuration out into config.pm
+       
+1997-12-09 01:54  jesse
+
+       * COPYING, Makefile, NEWS, README, TODO, etc/mysql.acl, etc/schema:
+
+       initial commit after i nuked the repository
+       
+1997-12-09 01:54  jesse
+
+       * COPYING, Makefile, NEWS, README, TODO, etc/mysql.acl, etc/schema:
+
+       Initial revision
+       
diff --git a/rt/Makefile b/rt/Makefile
new file mode 100644 (file)
index 0000000..fe01b71
--- /dev/null
@@ -0,0 +1,418 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/Makefile,v 1.1 2002-08-12 06:17:06 ivan Exp $
+# RT is Copyright 1996-2002 Jesse Vincent <jesse@bestpractical.com>
+# It is distributed under the terms of the GNU General Public License, version 2
+
+PERL                   =       /usr/bin/perl
+
+RT_VERSION_MAJOR       =       2
+RT_VERSION_MINOR       =       0
+RT_VERSION_PATCH       =       14
+
+
+RT_VERSION =   $(RT_VERSION_MAJOR).$(RT_VERSION_MINOR).$(RT_VERSION_PATCH)
+TAG       =    rt-$(RT_VERSION_MAJOR)-$(RT_VERSION_MINOR)-$(RT_VERSION_PATCH)
+
+BRANCH                 =       HEAD
+
+# This is the group that all of the installed files will be chgrp'ed to.
+RTGROUP                        =       rt
+
+
+# User which should own rt binaries.
+BIN_OWNER              =       root
+
+# User that should own all of RT's libraries, generally root.
+LIBS_OWNER             =       root
+
+# Group that should own all of RT's libraries, generally root.
+LIBS_GROUP             =       bin
+
+
+
+# {{{ Files and directories 
+
+# DESTDIR allows you to specify that RT be installed somewhere other than
+# where it will eventually reside
+
+DESTDIR                        =       
+
+
+# RT_PATH is the name of the directory you want make to install RT in
+# RT must be installed in its own directory (don't set this to /usr/local)
+
+RT_PATH                        =       /opt/rt2
+
+# The rest of these paths are all configurable, but you probably don't want to 
+# put them elsewhere
+
+RT_LIB_PATH            =       $(RT_PATH)/lib
+RT_ETC_PATH            =       $(RT_PATH)/etc
+RT_CONFIG_PATH         =       $(RT_ETC_PATH)
+RT_BIN_PATH            =       $(RT_PATH)/bin
+RT_MAN_PATH            =       $(RT_PATH)/man
+MASON_HTML_PATH                =       $(RT_PATH)/WebRT/html
+
+
+# RT allows sites to overlay the default web ui with 
+# local customizations Those files can be placed in MASON_LOCAL_HTML_PATH
+
+MASON_LOCAL_HTML_PATH  =       $(RT_PATH)/local/WebRT/html
+
+# RT needs to be able to write to MASON_DATA_PATH and MASON_SESSION_PATH
+# RT will create and chown these directories. Don't just set them to /tmp
+MASON_DATA_PATH                =       $(RT_PATH)/WebRT/data
+MASON_SESSION_PATH     =       $(RT_PATH)/WebRT/sessiondata
+
+RT_LOG_PATH             =       /tmp
+
+# RT_READABLE_DIR_MODE is the mode of directories that are generally meant
+# to be accessable
+RT_READABLE_DIR_MODE   =       0755
+
+
+
+# The location of your rt configuration file
+RT_CONFIG              =       $(RT_CONFIG_PATH)/config.pm
+
+# RT_MODPERL_HANDLER is the mason handler script for mod_perl
+RT_MODPERL_HANDLER     =       $(RT_BIN_PATH)/webmux.pl
+
+# RT_FASTCGI_HANDLER is the mason handler script for FastCGI
+# THIS HANDLER IS NOT CURRENTLY SUPPORTED
+RT_FASTCGI_HANDLER     =       $(RT_BIN_PATH)/mason_handler.fcgi
+
+# RT_SPEEDYCGI_HANDLER is the mason handler script for SpeedyCGI
+# THIS HANDLER IS NOT CURRENTLY SUPPORTED
+RT_SPEEDYCGI_HANDLER   =       $(RT_BIN_PATH)/mason_handler.scgi
+
+# The following are the names of the various binaries which make up RT 
+
+RT_CLI_BIN             =       $(RT_BIN_PATH)/rt
+RT_CLI_ADMIN_BIN       =       $(RT_BIN_PATH)/rtadmin
+RT_MAILGATE_BIN                =       $(RT_BIN_PATH)/rt-mailgate
+
+# }}}
+
+# {{{ Database setup
+
+#
+# DB_TYPE defines what sort of database RT trys to talk to
+# "mysql" is known to work.
+# "Pg" is known to work
+# "Oracle" is in the early stages of working.
+
+DB_TYPE                        =       mysql
+
+# DB_HOME is where the Database's commandline tools live.  $DB_HOME/bin
+# should contain the binaries themselves, e.g. if "which mysql" gives
+# "/usr/local/mysql/bin/mysql", $DB_HOME should be "/usr/local/mysql"
+
+DB_HOME                        = /usr
+
+# Set DBA to the name of a unix account with the proper permissions and 
+# environment to run your commandline SQL tools
+
+# Set DB_DBA to the name of a DB user with permission to create new databases 
+# Set DB_DBA_PASSWORD to that user's password (if you don't, you'll be prompted
+# later)
+
+# For mysql, you probably want 'root'
+# For Pg, you probably want 'postgres' 
+# For oracle, you want 'system'
+
+DB_DBA                 =       root
+DB_DBA_PASSWORD                =       
+#
+# Set this to the Fully Qualified Domain Name of your database server.
+# If the database is local, rather than on a remote host, using "localhost" 
+# will greatly enhance performance.
+
+DB_HOST                        =       localhost
+
+# If you're not running your database server on its default port, 
+# specifiy the port the database server is running on below.
+# It's generally safe to leave this blank 
+
+DB_PORT                        =       
+
+#
+# Set this to the canonical name of the interface RT will be talking to the 
+# database on.  If you said that the RT_DB_HOST above was "localhost," this 
+# should be too. This value will be used to grant rt access to the database.
+# If you want to access the RT database from multiple hosts, you'll need
+# to grant those database rights by hand.
+#
+
+DB_RT_HOST             =       localhost
+
+# set this to the name you want to give to the RT database in 
+# your database server. For Oracle, this should be the name of your sid
+
+DB_DATABASE            =       rt2
+
+# Set this to the name of the rt database user
+
+DB_RT_USER             =       rt_user
+
+# Set this to the password used by the rt database user
+# *** Change This Before Installation***
+
+DB_RT_PASS             =       rt_pass
+
+# }}}
+
+# {{{ Web configuration 
+
+# The user your webserver runs as. needed so that webrt can cache mason
+# objectcode
+
+WEB_USER               =       www
+WEB_GROUP              =       rt
+
+# }}}
+
+
+####################################################################
+# No user servicable parts below this line.  Frob at your own risk #
+####################################################################
+
+default:
+       @echo "Please read RT's readme before installing. Not doing so could"
+       @echo "be dangerous."
+
+install: dirs initialize.$(DB_TYPE) upgrade insert instruct
+
+instruct:
+       @echo "Congratulations. RT has been installed. "
+       @echo "You must now configure it by editing $(RT_CONFIG)."
+       @echo "From here on in, you should refer to the users guide."
+
+
+insert: insert-install
+       $(PERL) -I$(DESTDIR)/$(RT_ETC_PATH) -I$(DESTDIR)/$(RT_LIB_PATH) $(DESTDIR)/$(RT_ETC_PATH)/insertdata
+
+upgrade: dirs config-replace upgrade-noclobber  upgrade-instruct
+
+upgrade-instruct: 
+       @echo "Congratulations. RT has been upgraded. You should now check-over"
+       @echo "$(RT_CONFIG) for any necessary site customization. Additionally,"
+       @echo "you should update RT's system database objects by running "
+       @echo "    $(RT_ETC_PATH)/insertdata <version>"
+       @echo "where <version> is the version of RT you're upgrading from."
+
+upgrade-noclobber: insert-install libs-install html-install bin-install nondestruct
+
+nondestruct: fixperms
+
+testdeps:
+       $(PERL) ./tools/testdeps -warn $(DB_TYPE)
+
+fixdeps:
+       $(PERL) ./tools/testdeps -fix $(DB_TYPE)
+
+
+
+all:
+       @echo "Read the readme."
+
+fixperms:
+       # Make the libraries readable
+       chmod -R $(RT_READABLE_DIR_MODE) $(DESTDIR)/$(RT_PATH)
+       chown -R $(LIBS_OWNER) $(DESTDIR)/$(RT_LIB_PATH)
+       chgrp -R $(LIBS_GROUP) $(DESTDIR)/$(RT_LIB_PATH)
+
+       chown -R $(BIN_OWNER) $(DESTDIR)/$(RT_BIN_PATH)
+       chgrp -R $(RTGROUP) $(DESTDIR)/$(RT_BIN_PATH)
+
+
+       chmod $(RT_READABLE_DIR_MODE) $(DESTDIR)/$(RT_BIN_PATH)
+       chmod $(RT_READABLE_DIR_MODE) $(DESTDIR)/$(RT_BIN_PATH) 
+
+       chmod 0755 $(DESTDIR)/$(RT_ETC_PATH)
+       chmod 0500 $(DESTDIR)/$(RT_ETC_PATH)/*
+
+       #TODO: the config file should probably be able to have its
+       # owner set seperately from the binaries.
+       chown -R $(BIN_OWNER) $(DESTDIR)/$(RT_ETC_PATH)
+       chgrp -R $(RTGROUP) $(DESTDIR)/$(RT_ETC_PATH)
+
+       chmod 0550 $(DESTDIR)/$(RT_CONFIG)
+
+       # Make the interfaces executable and setgid rt
+       chown $(BIN_OWNER) $(DESTDIR)/$(RT_MAILGATE_BIN) \
+                       $(DESTDIR)/$(RT_FASTCGI_HANDLER) \
+                       $(DESTDIR)/$(RT_SPEEDYCGI_HANDLER) \
+                       $(DESTDIR)/$(RT_CLI_BIN) \
+                       $(DESTDIR)/$(RT_CLI_ADMIN_BIN)
+
+       chgrp $(RTGROUP) $(DESTDIR)/$(RT_MAILGATE_BIN) \
+                       $(DESTDIR)/$(RT_FASTCGI_HANDLER) \
+                       $(DESTDIR)/$(RT_SPEEDYCGI_HANDLER) \
+                       $(DESTDIR)/$(RT_CLI_BIN) \
+                       $(DESTDIR)/$(RT_CLI_ADMIN_BIN)
+
+       chmod 0755      $(DESTDIR)/$(RT_MAILGATE_BIN) \
+                       $(DESTDIR)/$(RT_FASTCGI_HANDLER) \
+                       $(DESTDIR)/$(RT_SPEEDYCGI_HANDLER) \
+                       $(DESTDIR)/$(RT_CLI_BIN) \
+                       $(DESTDIR)/$(RT_CLI_ADMIN_BIN)
+
+       chmod g+s       $(DESTDIR)/$(RT_MAILGATE_BIN) \
+                       $(DESTDIR)/$(RT_FASTCGI_HANDLER) \
+                       $(DESTDIR)/$(RT_SPEEDYCGI_HANDLER) \
+                       $(DESTDIR)/$(RT_CLI_BIN) \
+                       $(DESTDIR)/$(RT_CLI_ADMIN_BIN)
+
+       # Make the web ui readable by all. 
+       chmod -R  u+rwX,go-w,go+rX      $(DESTDIR)/$(MASON_HTML_PATH) \
+                                       $(DESTDIR)/$(MASON_LOCAL_HTML_PATH)
+       chown -R $(LIBS_OWNER)  $(DESTDIR)/$(MASON_HTML_PATH) \
+                               $(DESTDIR)/$(MASON_LOCAL_HTML_PATH)
+       chgrp -R $(LIBS_GROUP)  $(DESTDIR)/$(MASON_HTML_PATH) \
+                               $(DESTDIR)/$(MASON_LOCAL_HTML_PATH)
+
+       # Make the web ui's data dir writable
+       chmod 0770      $(DESTDIR)/$(MASON_DATA_PATH) \
+                       $(DESTDIR)/$(MASON_SESSION_PATH)
+       chown -R $(WEB_USER)    $(DESTDIR)/$(MASON_DATA_PATH) \
+                               $(DESTDIR)/$(MASON_SESSION_PATH)
+       chgrp -R $(WEB_GROUP)   $(DESTDIR)/$(MASON_DATA_PATH) \
+                               $(DESTDIR)/$(MASON_SESSION_PATH)
+dirs:
+       mkdir -p $(DESTDIR)/$(RT_BIN_PATH)
+       mkdir -p $(DESTDIR)/$(MASON_DATA_PATH)
+       mkdir -p $(DESTDIR)/$(MASON_SESSION_PATH)
+       mkdir -p $(DESTDIR)/$(RT_ETC_PATH)
+       mkdir -p $(DESTDIR)/$(RT_LIB_PATH)
+       mkdir -p $(DESTDIR)/$(MASON_HTML_PATH)
+       mkdir -p $(DESTDIR)/$(MASON_LOCAL_HTML_PATH)
+
+libs-install: 
+       [ -d $(DESTDIR)/$(RT_LIB_PATH) ] || mkdir $(DESTDIR)/$(RT_LIB_PATH)
+       chown -R $(LIBS_OWNER) $(DESTDIR)/$(RT_LIB_PATH)
+       chgrp -R $(LIBS_GROUP) $(DESTDIR)/$(RT_LIB_PATH)
+       chmod -R $(RT_READABLE_DIR_MODE) $(DESTDIR)/$(RT_LIB_PATH)
+       ( cd ./lib; \
+         $(PERL) Makefile.PL INSTALLSITELIB=$(DESTDIR)/$(RT_LIB_PATH) \
+                             INSTALLMAN1DIR=$(DESTDIR)/$(RT_MAN_PATH)/man1 \
+                             INSTALLMAN3DIR=$(DESTDIR)/$(RT_MAN_PATH)/man3 \
+           && make \
+           && make test \
+           && $(PERL) -p -i -e " s'!!RT_VERSION!!'$(RT_VERSION)'g;" blib/lib/RT.pm ;\
+           make install \
+                          INSTALLSITEMAN1DIR=$(DESTDIR)/$(RT_MAN_PATH)/man1 \
+                          INSTALLSITEMAN3DIR=$(DESTDIR)/$(RT_MAN_PATH)/man3 \
+       )
+
+html-install:
+       cp -rp ./webrt/* $(DESTDIR)/$(MASON_HTML_PATH)
+
+
+
+genschema:
+       $(PERL) tools/initdb '$(DB_TYPE)' '$(DB_HOME)' '$(DB_HOST)' '$(DB_PORT)' '$(DB_DBA)' '$(DB_DATABASE)' generate
+
+
+initialize.Pg: createdb initdb.dba acls 
+
+initialize.mysql: createdb acls initdb.rtuser
+
+initialize.Oracle: acls initdb.rtuser
+
+acls:
+       cp etc/acl.$(DB_TYPE) '$(DESTDIR)/$(RT_ETC_PATH)/acl.$(DB_TYPE)'
+       $(PERL) -p -i -e " s'!!DB_TYPE!!'"$(DB_TYPE)"'g;\
+                               s'!!DB_HOST!!'"$(DB_HOST)"'g;\
+                               s'!!DB_RT_PASS!!'"$(DB_RT_PASS)"'g;\
+                               s'!!DB_RT_HOST!!'"$(DB_RT_HOST)"'g;\
+                               s'!!DB_RT_USER!!'"$(DB_RT_USER)"'g;\
+                               s'!!DB_DATABASE!!'"$(DB_DATABASE)"'g;" $(DESTDIR)/$(RT_ETC_PATH)/acl.$(DB_TYPE)
+       bin/initacls.$(DB_TYPE) '$(DB_HOME)' '$(DB_HOST)' '$(DB_PORT)' '$(DB_DBA)' '$(DB_DBA_PASSWORD)' '$(DB_DATABASE)' '$(DESTDIR)/$(RT_ETC_PATH)/acl.$(DB_TYPE)' 
+
+
+
+dropdb: 
+       $(PERL) tools/initdb '$(DB_TYPE)' '$(DB_HOME)' '$(DB_HOST)' '$(DB_PORT)' '$(DB_DBA)' '$(DB_DATABASE)' drop
+
+
+createdb: 
+       $(PERL) tools/initdb '$(DB_TYPE)' '$(DB_HOME)' '$(DB_HOST)' '$(DB_PORT)' '$(DB_DBA)' '$(DB_DATABASE)' create
+initdb.dba:
+       $(PERL) tools/initdb '$(DB_TYPE)' '$(DB_HOME)' '$(DB_HOST)' '$(DB_PORT)' '$(DB_DBA)' '$(DB_DATABASE)' insert
+
+initdb.rtuser:
+       $(PERL) tools/initdb '$(DB_TYPE)' '$(DB_HOME)' '$(DB_HOST)' '$(DB_PORT)' '$(DB_RT_USER)' '$(DB_DATABASE)' insert
+
+
+
+insert-install:
+       cp -rp ./tools/insertdata \
+                $(DESTDIR)/$(RT_ETC_PATH)
+       $(PERL) -p -i -e " s'!!RT_ETC_PATH!!'$(RT_ETC_PATH)'g;\
+                          s'!!RT_LIB_PATH!!'$(RT_LIB_PATH)'g;"\
+               $(DESTDIR)/$(RT_ETC_PATH)/insertdata
+
+bin-install:
+       cp -p ./bin/webmux.pl $(DESTDIR)/$(RT_MODPERL_HANDLER)
+       cp -p ./bin/rt-mailgate $(DESTDIR)/$(RT_MAILGATE_BIN)
+       cp -p ./bin/rtadmin $(DESTDIR)/$(RT_CLI_ADMIN_BIN)
+       cp -p ./bin/rt $(DESTDIR)/$(RT_CLI_BIN)
+       cp -p ./bin/mason_handler.fcgi $(DESTDIR)/$(RT_FASTCGI_HANDLER)
+       cp -p ./bin/mason_handler.scgi $(DESTDIR)/$(RT_SPEEDYCGI_HANDLER)
+
+       $(PERL) -p -i -e "s'!!RT_PATH!!'"$(RT_PATH)"'g;\
+                               s'!!PERL!!'"$(PERL)"'g;\
+                               s'!!RT_VERSION!!'"$(RT_VERSION)"'g;\
+                               s'!!RT_ETC_PATH!!'"$(RT_CONFIG_PATH)"'g;\
+                               s'!!RT_LIB_PATH!!'"$(RT_LIB_PATH)"'g;"\
+               $(DESTDIR)/$(RT_MODPERL_HANDLER) \
+               $(DESTDIR)/$(RT_FASTCGI_HANDLER) \
+               $(DESTDIR)/$(RT_SPEEDYCGI_HANDLER) \
+               $(DESTDIR)/$(RT_CLI_BIN) \
+               $(DESTDIR)/$(RT_CLI_ADMIN_BIN) \
+               $(DESTDIR)/$(RT_MAILGATE_BIN)
+
+
+config-replace:
+       -[ -f $(DESTDIR)/$(RT_CONFIG) ] && \
+               mv $(DESTDIR)/$(RT_CONFIG) $(DESTDIR)/$(RT_CONFIG).old && \
+               chmod 000 $(DESTDIR)/$(RT_CONFIG).old
+       cp -rp ./etc/config.pm $(DESTDIR)/$(RT_CONFIG)
+       $(PERL) -p -i -e "\
+       s'!!DB_TYPE!!'"$(DB_TYPE)"'g;\
+       s'!!DB_HOST!!'"$(DB_HOST)"'g;\
+       s'!!DB_PORT!!'"$(DB_PORT)"'g;\
+       s'!!DB_RT_PASS!!'"$(DB_RT_PASS)"'g;\
+       s'!!DB_RT_USER!!'"$(DB_RT_USER)"'g;\
+       s'!!DB_DATABASE!!'"$(DB_DATABASE)"'g;\
+       s'!!MASON_HTML_PATH!!'"$(MASON_HTML_PATH)"'g;\
+       s'!!MASON_LOCAL_HTML_PATH!!'"$(MASON_LOCAL_HTML_PATH)"'g;\
+       s'!!MASON_SESSION_PATH!!'"$(MASON_SESSION_PATH)"'g;\
+       s'!!MASON_DATA_PATH!!'"$(MASON_DATA_PATH)"'g;\
+       s'!!RT_LOG_PATH!!'"$(RT_LOG_PATH)"'g;\
+       s'!!RT_VERSION!!'"$(RT_VERSION)"'g;\
+       " $(DESTDIR)/$(RT_CONFIG)
+
+
+commit:
+       cvs commit
+
+predist: commit
+       cvs tag -r $(BRANCH) -F $(TAG)
+       rm -rf /tmp/$(TAG)
+       cvs co -d /tmp/$(TAG) -r $(TAG) rt
+       cd /tmp/$(TAG); chmod 600 Makefile; /usr/local/bin/cvs2cl.pl \
+               --no-wrap --separate-header \
+               --window 120
+       cd /tmp; tar czvf /home/ftp/pub/rt/devel/$(TAG).tar.gz $(TAG)/
+       chmod 644 /home/ftp/pub/rt/devel/$(TAG).tar.gz
+
+dist: commit predist
+       rm -rf /home/ftp/pub/rt/devel/rt.tar.gz
+       ln -s ./$(TAG).tar.gz /home/ftp/pub/rt/devel/rt.tar.gz
+
+
+rpm:
+       (cd ..; tar czvf /usr/src/redhat/SOURCES/rt.tar.gz rt)
+       rpm -ba etc/rt.spec
diff --git a/rt/README b/rt/README
new file mode 100755 (executable)
index 0000000..d16100c
--- /dev/null
+++ b/rt/README
@@ -0,0 +1,336 @@
+$Header: /home/cvs/cvsroot/freeside/rt/README,v 1.1 2002-08-12 06:17:06 ivan Exp $
+RT is (c) 1996-2002 by Jesse Vincent <jesse@bestpractical.com>
+
+RT is licensed to you under the terms of version 2 of the GNU General 
+Public License. 
+
+If you don't have a copy of the GPL, you've been living in a cave,
+but one should be included in this distribution.
+
+
+INSTALLATION INSTRUCTIONS
+-------------------------
+
+These instructions are a summary of those at http://www.fsck.com/rtfm/
+The docs on the web at www.fsck.com/rtfm/ are likely to be more up to
+date and complete than this document. You should consult them before 
+proceeding.
+
+REQUIRED PACKAGES:
+------------------
+
+o   Perl5.005_03 or later with support for setgid perl scripts
+        RT's command line and mail gateway tools run setgid to the 'rt' group
+       to protect RT's database password.  You may need to install a special 
+       "suidperl" package or reconfigure your perl setup to support
+        "setuid scripts".
+
+o   A DB backend; MySQL is recommended ( http://www.mysql.com ) 
+        Currently supported:    Mysql 3.23.38 or newer. 
+                                (Some older releases had crippling SQL bugs)
+                               Postgres 7.1 or newer.
+
+o   Apache + mod_perl -- ( http://perl.apache.org) 
+    or A webserver with FastCGI support (www.fastcgi.com)
+
+       If you compile mod_perl as a DSO, you're on your own. It's known
+       to have massive stability problems. 
+        mod_perl must be build with EVERYTHING=1
+
+o    Various and sundry perl modules
+        RT takes care of the installation of most of these automatically
+        during the "make testdeps" and "make fixdeps" stages below
+
+
+GENERAL INSTALLATION
+--------------------
+
+1   Unpack this distribution SOMWHERE OTHER THAN where you want to install RT
+
+        Granted, you've already got it open. To do this cleanly:
+
+               tar xzvf rt.tar.gz -C /tmp
+
+2   Check over /tmp/rt/Makefile
+
+       There are many variables you NEED to customize for your site.
+       Even if you are just upgrading, you must set ALL variables.
+
+3   Satisfy RT's myriad dependencies.  There's a perl script in rt/tools
+    called testdeps that uses CPAN to automate all of this.
+
+3.1   Check for compliance:
+       make testdeps
+
+3.2   If there are unsatisfied dependencies, install them by hand or run
+       make fixdeps
+       
+       (You may need to install Apache::Session and Apache::DBI by hand.
+
+       You might need to install Msql-Mysql-Modules by hand.
+       perl -MCPAN -e'install DBD::mysql::Install' should do it for you.
+       )
+
+3.3   Check to make sure everything was installed properly:
+       make testdeps
+
+4   Create a group called 'rt'
+
+5a  FOR A NEW INSTALLATION: 
+        
+        As root, type:
+                make install   (replace "make" with the local name for 
+                                Make, if you need to)
+
+       If the make fails, type:
+               make dropdb 
+       and start over from step 5a
+
+5b  FOR UPGRADING: (Within the RT 2.0.x series)
+
+       Make a backup of /path/to/rt/etc/config.pm
+        As root, type: 
+               make upgrade     (replace "make" with the local name for 
+                                 Make, if you need to)
+
+       This will build new binaries, config files and libraries without
+       overwriting your RT database. 
+
+        WARNING: This WILL clobber your existing configuration file!
+        
+        The install process will then instruct you to update your RT system 
+        database objects by running rt/etc/insertdata <version> where 
+        <version> is the version of RT you're upgrading from.
+
+        
+       
+5c  FOR UPGRADING (From 1.0.x):
+
+       Follow the instructions for installing RT 2.0.
+
+       Once you have installed RT 2.0, download import-1.0-to-2.0
+       from http://www.fsck.com/pub/rt/contrib/2.0/rt-addons
+
+       Edit the configuration defaults in import-1.0-to-2.0
+
+       If you don't set $DEFAULTQUEUE to the name of one of your
+       RT 1.0 queues, THE IMPORT WILL FAIL.
+
+       perl ./import-1.0-to-2.0
+
+       The import tool will do its thing. If you're using postgres, you'll
+       need to execute the following SQL statement within your RT2 database:
+
+       select setval('tickets_id_seq', (select max(id) from tickets));
+       
+       It imports:
+               Queues, Areas, Users, Acls, Mailing Rules, Queue Members,
+               Tickets and Transactions.
+
+       It DOES NOT IMPORT:
+               Attachments removed by stripmime or Templates.
+       
+6   Edit etc/config.pm in your RT installation directory.  In many
+    cases sensible defaults have been included. In others, you MUST
+    supply a value.
+
+7   Configure the email and web gateways, as described below. 
+
+8   Stop and start your webserver, so it picks up your configuration changes.
+
+    NOTE: root's password for the web interface is "password" 
+    (without the quotes.)  Not changing this is a SECURITY risk
+    
+9   Configure RT per the instructions at http://www.fsck.com/rtfm/
+
+    Until you do this, RT will not be able to send or recieve email,
+    nor will it be more than marginally functional.  This is not an
+    optional step.
+
+
+SETTING UP THE MAIL GATEWAY 
+---------------------------
+
+An alias for the initial queue will need to be made in either your
+global mail aliases file (if you are using NIS) or locally on your
+machine.
+Add the following lines to /etc/aliases (or your local equivalent) :
+
+rt:         "|/path/to/rt2/bin/rt-mailgate --queue general --action correspond"
+rt-comment: "|/path/to/rt2/bin/rt-mailgate --queue general --action comment"
+                                                   |                |
+                                   <queue-name>----/                |
+                                                                    |
+                      <correspond or comment depending on whether   |
+                      the mail should be resent to the requestor>---/
+
+
+
+THE WEB UI
+----------
+
+RT's web ui is based around HTML::Mason, which works well with the mod_perl
+perl interpreter within Apache httpd as well as with a webserver which
+supports FastCGI. (Instructions for configuring RT for use with FastCGI
+are available at http://www.fsck.com/rtfm/ )
+
+Apache 
+        RT Uses HTML::Mason.  You'll need to add a few lines to your
+        httpd.conf telling it to use rt's web ui.  If you have mod-perl
+       (you should, the perl scripts will go quite a bit faster around with
+       it), you can do something like this:
+
+
+<VirtualHost your.ip.address>
+DocumentRoot /path/to/rt2/WebRT/html
+ServerName your.rt.server.hostname
+PerlModule Apache::DBI
+PerlFreshRestart On
+PerlRequire /path/to/rt2/bin/webmux.pl
+<Location />
+ SetHandler perl-script
+ PerlHandler RT::Mason
+</Location>
+</VirtualHost>
+
+Additionally, you should set up a cron job to remove stale session data.
+
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ WARNING: Don't install this cron job or run this find command if your
+ MASON_SESSION_PATH (known in config.pm as $MasonSessionDir) 
+ points to a directory that could  EVER contain any file that's not 
+ a Apache::Session datafile.
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+# Every hour, nuke session files and lockfiles that haven't been 
+# touched in 10 hours
+
+0 * * * * find /path/to/rt2/WebRT/sessiondata -type f -amin +600 -exec rm {} \;
+
+
+THE CLI
+-------
+        Binaries for the CLI are located in rt/bin
+        You've got:
+
+                "rt" (manipulate or display requests) 
+                "rtadmin" (modify queues, users and acls)
+
+        Both of these programs take --help as an option.
+
+
+BUGS
+----
+
+Known issues with releases of RT2 are listed at 
+<URL:http://fsck.com/rt2/NoAuth/Errata.html>.  This includes every bug known
+to exist in each release of RT.  (When prompted, login as guest/guest)
+
+To find out more about currently open bugs, check out the live 
+Buglist at  <URL:http://fsck.com/rt2/NoAuth/Buglist.html>.
+(When prompted, login as guest/guest)
+
+To report a bug, send an email to rt-2.0-bugs@fsck.com.
+
+GETTING HELP
+------------
+
+If RT is mission-critical for you or if you use it heavily, we recommend that
+you purchase a commercial support contract.  Details on support contracts
+are available at http://www.bestpractical.com.
+
+If you're interested in having RT extended or customized or would like more
+information about commercial support options, please send email to 
+<sales@bestpractical.com> to discuss rates and availability.
+
+
+RT-USERS MAILINGLIST
+--------------------
+
+To keep up to date on the latest RT tips, techniques and extections,
+you probably want to join the rt-users mailinglist.  Send a message to:
+
+         rt-users-request@lists.fsck.com 
+
+With the body of the message consisting of only the word:
+
+        subscribe
+
+If you're interested in hacking on rt, you'll want to subscribe to
+rt-devel@lists.fsck.com.  Subscribe to it with instructions similar to
+those above.
+
+Address questions about the stable release to the rt-users list, and
+questions about the development version to the rt-devel list.  If you feel
+your questions are best not asked publically, send them personally to
+<jesse@bestpractical.com>.
+
+If you want to be informed of every commit to the CVS repository,
+subscribe to rt-commit@fsck.com using similar instructions to those above.
+
+
+RT WEBSITE
+----------
+
+For current information about RT, check out the RT website at 
+http://www.bestpractical.com/rt  You'll find screenshots, a pointer
+to the current version of rt, contributed patches and lots of other great
+stuff.
+
+
+TROUBLESHOOTING
+---------------
+
+All errors will be appended to a logfile, which lives in /tmp/rt.log.* unless 
+you've reconfigured it.  Check etc/config.pm for details.
+
+If the solution to the problem you're running into isn't obvious and you've 
+checked the FAQ, feel free to send mail to rt-users@fsck.com (for release 
+versions of RT) or rt-devel@fsck.com (for development versions).
+
+GIVING SOMETHING BACK
+---------------------
+
+RT is free software. You are not obligated to pay for it.  You should be 
+aware, however, that bestpractical.com's sole source of revenue is commercial
+work related to RT. If you are able, either a contract to extend RT in some 
+way that would be useful to your organization, a financial contribution, or 
+even something off the author's amazon wishlist 
+       ( http://www.amazon.com/exec/obidos/wishlist/2GMHUDAFBT2XR/ )
+would be much appreciated. 
+
+Thanks!
+
+
+CREDITS
+-------
+
+A lot of people are responsible for making RT a better program.  Many
+thanks to Lauren Burka, who originally tasked me with writing this beast.
+She forced me to use a database backend.  I've thanked her for it every
+day since.  Rich West rewrote this readme and did some UI hacking.  Adam
+Hirsch, Kit Kraysha, Robin Garner, Jens Glaser, John Adams, Trey Belew, 
+Sean Dague, Nathan Mehl, Kee Hinckley, Rich West, Dale Bewley, Serge Zhuk,
+John Lengeling, Elmar Knipp, Gerald Abshez, Dave Hull, Dave Schenet,
+Dave Walton, Jan Okrouhly, Tobias Brox, Lamont Lucas, Charlie Brady,
+Robin Shostack, Eric Mumpower, Jerrod Wiesman, Adam Hammer, Ivan Kohler, Alex
+Pilosov, Mary Alderdice, Deborah Kaplan, Jens von Bülow, Tristan Horn,
+Lee Ann Goldstein, Karel P Kerezman, Feargal Reilly, Christian Steger,
+Christian Kurz, JD Falk, Arthur de Jong, Ben Carter, Mark Vevers
+and many others
+have all contributed bug reports, code or ideas that have helped RT along.  
+
+Arepa, Inc, Utopia Inc, Wesleyan University and The Leftbank Operation 
+have paid me to maintain RT and release it to the public.  Without their 
+support RT would not exist.  
+
+If I've left you out, please drop me a line ....it wasn't intentional. 
+
+        Enjoy
+
+        Jesse Vincent
+       <jesse@bestpractical.com>
+        Best Practical Solutions, LLC
diff --git a/rt/TODO b/rt/TODO
new file mode 100755 (executable)
index 0000000..3d1b7d0
--- /dev/null
+++ b/rt/TODO
@@ -0,0 +1,9 @@
+Errata for RT 2.x can be found at http://fsck.com/rt2/NoAuth/Errata.html
+
+A list of all open issues, including those which haven't been added
+to the official Errata lists,  can be found at 
+http://fsck.com/rt2/NoAuth/Buglist.html
+
+If you want to look at bugs in more detail, you may need to login as guest with a password of 'guest'
+
+
diff --git a/rt/bin/initacls.Oracle b/rt/bin/initacls.Oracle
new file mode 100644 (file)
index 0000000..8d05f45
--- /dev/null
@@ -0,0 +1,26 @@
+#!/bin/sh
+
+DATABASEHOME=$1
+HOSTNAME=$2
+PORT=$3
+DATABASEADMIN=$4
+DBAPASSWD=$5
+DATABASENAME=$6
+DATABASEACLS=$7
+
+BINDIR=${DATABASEHOME}/bin
+
+echo "DBHOME = $DATABASEHOME"
+echo "HOSTNAME = $HOSTNAME"
+echo "PORT = $PORT"
+echo "DATABASEADMIN = $DATABASEADMIN"
+echo "DBAPASSWD = $DBAPASSWD"
+echo "DATABASENAME = $DATABASENAME"
+
+PATH=$PATH:$BINDIR
+export PATH
+
+echo "Please enter ${DATABASEADMIN}'s  password for the SID ${DATABASENAME} to create an rt user";
+
+$BINDIR/sqlplus ${DATABASEADMIN}@${DATABASENAME}  @$DATABASEACLS
+
diff --git a/rt/bin/initacls.Pg b/rt/bin/initacls.Pg
new file mode 100755 (executable)
index 0000000..82e32de
--- /dev/null
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+DATABASEHOME=$1
+HOSTNAME=$2
+PORT=$3
+DATABASEADMIN=$4
+DBAPASSWD=$5
+DATABASENAME=$6
+DATABASEACLS=$7
+
+BINDIR=${DATABASEHOME}/bin
+
+
+PATH=$PATH:$BINDIR
+export PATH
+
+echo "Enter the postgres administrator's database password to create a new user for rt"
+
+if [ "fnord$PORT" != "fnord" ]; then
+       PORT="-p $PORT"
+fi;
+
+if [ "fnord$HOSTNAME" != "fnord" ]; then
+       HOSTNAME="-h $HOSTNAME"
+fi;
+
+psql $HOSTNAME $PORT -d $DATABASENAME -f $DATABASEACLS -U $DATABASEADMIN
+
diff --git a/rt/bin/initacls.mysql b/rt/bin/initacls.mysql
new file mode 100755 (executable)
index 0000000..17e63f8
--- /dev/null
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+DATABASEHOME=$1
+HOSTNAME=$2
+PORT=$3
+DATABASEADMIN=$4
+DBAPASSWD=$5
+DATABASENAME=$6
+DATABASEACLS=$7
+
+BINDIR=${DATABASEHOME}/bin
+
+PATH=$PATH:$BINDIR
+export PATH
+
+echo "Enter the mysql administrator's database password to create a new user for RT"
+$BINDIR/mysql --host=${HOSTNAME} --port=${PORT} --user=${DATABASEADMIN} -p${DBAPASSWD} mysql < $DATABASEACLS
+
+echo "Enter the mysql administrator's database password to nondestructively reload the database"
+$BINDIR/mysqladmin --host=${HOSTNAME} --port=${PORT} --user=${DATABASEADMIN} -p${DBAPASSWD} reload
diff --git a/rt/bin/mason_handler.fcgi b/rt/bin/mason_handler.fcgi
new file mode 100755 (executable)
index 0000000..e8a4e12
--- /dev/null
@@ -0,0 +1,221 @@
+#!!!PERL!!
+# $Header: /home/cvs/cvsroot/freeside/rt/bin/mason_handler.fcgi,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# RT is (c) 1996-2001 Jesse Vincent (jesse@fsck.com);
+
+use strict;
+$ENV{'PATH'} = '/bin:/usr/bin';    # or whatever you need
+$ENV{'CDPATH'} = '' if defined $ENV{'CDPATH'};
+$ENV{'SHELL'} = '/bin/sh' if defined $ENV{'SHELL'};
+$ENV{'ENV'} = '' if defined $ENV{'ENV'};
+$ENV{'IFS'} = ''          if defined $ENV{'IFS'};
+
+
+# We really don't want apache to try to eat all vm
+# see http://perl.apache.org/guide/control.html#Preventing_mod_perl_Processes_Fr
+
+
+package RT::Mason;
+#use CGI qw(-private_tempfiles);   # pull in CGI with the private tempfiles
+                                 #option predefined
+use HTML::Mason;  # brings in subpackages: Parser, Interp, etc.
+
+use vars qw($VERSION %session $Nobody $SystemUser $cgi);
+
+# List of modules that you want to use from components (see Admin
+# manual for details)
+
+#Clean up our umask...so that the session files aren't world readable, writable or executable
+umask(0077);
+
+
+  
+$VERSION="!!RT_VERSION!!";
+
+use lib "!!RT_LIB_PATH!!";
+use lib "!!RT_ETC_PATH!!";
+
+#This drags in  RT's config.pm
+use config;
+use Carp;
+
+{  
+    package HTML::Mason::Commands;
+    use vars qw(%session $ContentType);
+    
+    use RT; 
+    use RT::Ticket;
+    use RT::Tickets;
+    use RT::Transaction;
+    use RT::Transactions;
+    use RT::User;
+    use RT::Users;
+    use RT::CurrentUser;
+    use RT::Template;
+    use RT::Templates;
+    use RT::Queue;
+    use RT::Queues;
+    use RT::ScripAction;
+    use RT::ScripActions;
+    use RT::ScripCondition;
+    use RT::ScripConditions;
+    use RT::Scrip;
+    use RT::Scrips;
+    use RT::Group;
+    use RT::Groups;
+    use RT::Keyword;
+    use RT::Keywords;
+    use RT::ObjectKeyword;
+    use RT::ObjectKeywords;
+    use RT::KeywordSelect;
+    use RT::KeywordSelects;
+    use RT::GroupMember;
+    use RT::GroupMembers;
+    use RT::Watcher;
+    use RT::Watchers;
+    use RT::Handle;
+    use RT::Interface::Web;    
+    use MIME::Entity;
+    use CGI::Cookie;
+    use Date::Parse;
+    use HTML::Entities;
+    use Text::Wrapper;
+    #TODO: make this use DBI
+    use Apache::Session::File;
+    use CGI::Fast;
+
+    # set the page's content type.
+    # In this case, just save it to a variable that we can pull later;
+    sub SetContentType {
+       $ContentType = shift;
+    }
+    sub CGIObject {
+       return $RT::Mason::cgi;
+    }
+}
+
+
+my ($output, $parser, $interp);
+if ($HTML::Mason::VERSION < 1.0902) {
+        require HTML::Mason::ApacheHandler;
+
+         $parser = &RT::Interface::Web::NewParser(allow_globals => [%session]);
+
+         $interp = &RT::Interface::Web::NewInterp(parser=>$parser,
+                                             allow_recursive_autohandlers => 1,
+                                           out_method => \$output);
+}
+else {
+         $interp = &RT::Interface::Web::NewInterp(
+                                                  allow_globals => [%session],
+                                                  default_escape_flags => 'h',
+
+                                           out_method => \$output);
+}
+# Die if WebSessionDir doesn't exist or we can't write to it
+
+stat ($RT::MasonSessionDir);
+die "Can't read and write $RT::MasonSessionDir"
+  unless (( -d _ ) and ( -r _ ) and ( -w _ ));
+
+
+RT::Init();
+
+# Response loop
+while ($RT::Mason::cgi = new CGI::Fast) {
+    
+    $HTML::Mason::Commands::ContentType = 'text/html';
+        
+    # This routine comes from ApacheHandler.pm:
+    my (%args, $cookie);
+    foreach my $key ( $cgi->param ) {
+       foreach my $value ( $cgi->param($key) ) {
+           if (exists($args{$key})) {
+               if (ref($args{$key})) {
+                   $args{$key} = [@{$args{$key}}, $value];
+               } else {
+                   $args{$key} = [$args{$key}, $value];
+               }
+           } else {
+               $args{$key} = $value;
+           }
+
+       }
+       
+    }
+    
+
+    my $comp = $ENV{'PATH_INFO'};
+    
+    if ($comp =~ /^(.*)$/) {  # untaint the path info. apache should
+                             # never hand us a bogus path. 
+                             # We should be more careful here.
+       $comp = $1;
+    }    
+    
+    if ($comp =~ /\/$/) {
+       $comp .= "index.html";
+    }  
+    
+    #This is all largely cut and pasted from mason's session_handler.pl
+    
+    # {{{ Cookies
+    my %cookies = fetch CGI::Cookie();
+    
+    eval {
+       my $session_id = undef;
+
+       #Get the session id and untaint it
+       if ($cookies{'AF_SID'} && $cookies{'AF_SID'}->value() =~ /^(.*)$/) {
+               $session_id = $1;
+       }
+       tie %HTML::Mason::Commands::session, 'Apache::Session::File',
+               $session_id, 
+           { Directory => $RT::MasonSessionDir,
+             LockDirectory => $RT::MasonSessionDir,
+           }   ;
+    };
+    
+    if ( $@ ) {
+       # If the session is invalid, create a new session.
+       if ( $@ =~ m#^Object does not exist in the data store# ) {
+            tie %HTML::Mason::Commands::session, 'Apache::Session::File', undef,
+            { Directory => $RT::MasonSessionDir,
+              LockDirectory => $RT::MasonSessionDir,
+            };
+            undef $cookies{'AF_SID'};
+       }
+         else {
+             die "$@ \nProbably means that RT Couldn't write to session directory '$RT::MasonSessionDir'. Check that this directory's permissions are correct.";
+         }
+    }
+    
+    if ( !$cookies{'AF_SID'} ) {
+       $cookie = new CGI::Cookie
+         (-name=>'AF_SID', 
+          -value=>$HTML::Mason::Commands::session{_session_id}, 
+          -path => '/',);
+       
+    } else {
+       $cookie = undef;
+    }
+    
+    # }}}
+    
+    $output = '';
+    eval {
+           my $status = $interp->exec($comp, %args);
+    };
+    if ($@) {
+       $output = "<PRE>$@</PRE>";
+    }
+    print "Content-Type: $HTML::Mason::Commands::ContentType\r\n";
+    print "Set-Cookie: $cookie\r\n" if ($cookie);
+    print "\r\n";
+    print $output;
+    untie %HTML::Mason::Commands::session;
+    
+}
diff --git a/rt/bin/mason_handler.scgi b/rt/bin/mason_handler.scgi
new file mode 100755 (executable)
index 0000000..b9846c8
--- /dev/null
@@ -0,0 +1,193 @@
+#!!!PERL!! -w
+
+#!/usr/bin/speedy -- -t600 -M8
+# $Header: /home/cvs/cvsroot/freeside/rt/bin/mason_handler.scgi,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# RT is (c) 1996-2001 Jesse Vincent (jesse@fsck.com);
+#
+# Contains code derived from mason.cgi
+# mason.cgi is Copyright December 2000 Joshua Kronengold (mneme@io.com, 
+# mneme@cyberspace.org).  All Rights Reserved.
+
+use strict;
+# {{{ Clean out the environment a little bit
+$ENV{'PATH'} = '/bin:/usr/bin';    # or whatever you need
+$ENV{'CDPATH'} = '' if defined $ENV{'CDPATH'};
+$ENV{'SHELL'} = '/bin/sh' if defined $ENV{'SHELL'};
+$ENV{'ENV'} = '' if defined $ENV{'ENV'};
+$ENV{'IFS'} = ''          if defined $ENV{'IFS'};
+# }}}
+
+package RT::Mason;
+use HTML::Mason;  # brings in subpackages: Parser, Interp, etc.
+use vars qw($VERSION %session $Nobody $SystemUser);
+
+# List of modules that you want to use from components (see Admin
+# manual for details)
+
+$VERSION="!!RT_VERSION!!";
+
+use lib "!!RT_LIB_PATH!!";
+use lib "!!RT_ETC_PATH!!";
+
+
+#This drags in  RT's config.pm
+use config;
+use Carp;
+
+use HTML::Mason::FakeApache;
+use CGI;
+
+# {{{ Set up CGI environment and grab CGI params:
+
+my $r=new HTML::Mason::FakeApache;
+
+$|=1; # set output to non-buffered.
+
+my %cgi;
+CGI::ReadParse(\%cgi); # %cgi is now a tied hash containing our params.
+
+my $q=$cgi{CGI}; # $q now contains the object tied to %cgi.
+# }}}
+
+# {{{ require what we need
+{  
+    package HTML::Mason::Commands;
+
+    use vars qw(%session);
+
+    use RT::Ticket;
+    use RT::Tickets;
+    use RT::Transaction;
+    use RT::Transactions;
+    use RT::User;
+    use RT::Users;
+    use RT::CurrentUser;
+    use RT::Template;
+    use RT::Templates;
+    use RT::Queue;
+    use RT::Queues;
+    use RT::ScripAction;
+    use RT::ScripActions;
+    use RT::ScripCondition;
+    use RT::ScripConditions;
+    use RT::Scrip;
+    use RT::Scrips;
+    use RT::Group;
+    use RT::Groups;
+    use RT::Keyword;
+    use RT::Keywords;
+    use RT::ObjectKeyword;
+    use RT::ObjectKeywords;
+    use RT::KeywordSelect;
+    use RT::KeywordSelects;
+    use RT::GroupMember;
+    use RT::GroupMembers;
+    use RT::Watcher;
+    use RT::Watchers;
+    use RT::Handle;
+    use RT::Interface::Web;    
+    use MIME::Entity;
+    use CGI::Cookie;
+    use Date::Parse;
+    use HTML::Entities;
+
+    
+    use Apache::Session::File;
+
+    
+}
+# }}}
+
+# {{{ RT Database setup
+    $RT::Handle = new RT::Handle;
+    
+    $RT::Handle->Connect;
+   
+    use RT::CurrentUser;
+    
+    #RT's system user is a genuine database user. its id lives here
+    $RT::SystemUser = new RT::CurrentUser();
+    $RT::SystemUser->LoadByName('RT_System');
+
+    #RT's "nobody user" is a genuine database user. its ID lives here.
+    $RT::Nobody = new RT::CurrentUser();
+    $RT::Nobody->LoadByName('Nobody'); 
+     
+
+# }}}
+
+
+
+
+# {{{ Deal with cookies
+
+my %cookies = fetch CGI::Cookie();
+eval { 
+    tie %HTML::Mason::Commands::session, 'Apache::Session::File',
+      ( $cookies{'AF_SID'} ? $cookies{'AF_SID'}->value() : undef );
+};
+
+if ( $@ ) {
+    # If the session is invalid, create a new session.
+    if ( $@ =~ m#^Object does not exist in the data store# ) {
+        tie %HTML::Mason::Commands::session, 'Apache::Session::File', undef;
+        undef $cookies{'AF_SID'};
+    }
+}
+
+if ( !$cookies{'AF_SID'} ) {
+    my $cookie = new CGI::Cookie(
+        -name=>'AF_SID', 
+        -value=>$HTML::Mason::Commands::session{_session_id}, 
+         -path => '/');
+    print 'Set-Cookie: '. $cookie."\r\n";
+}
+
+# }}}
+
+my $path=$ENV{PATH_INFO} || "/"; $path=~s/\'/\\\'/g;
+
+my $type=`/usr/bin/file '$RT::MasonComponentRoot/$path'`;
+
+# {{{ if it's a text file, handle it with mason.
+if($type=~/text|directory/) { 
+       my ($out, %mason_params);
+        my $parser = RT::Interface::Web::NewParser(allow_globals=>[qw($r)]);
+       $mason_params{parser}=$parser;
+       $r->content_type('text/html');
+       # (get cookies line) ...
+       $r->access_hash('headers_in','Cookie',$ENV{HTTP_COOKIE});
+       $r->{'args@'}=[];
+       $mason_params{out_method}=\$out;
+
+       my $interp = RT::Interface::Web::NewInterp(%mason_params);
+
+       $interp->set_global(r=>$r);
+       $interp->exec($path,%cgi);
+       $r->send_http_header();
+        print $out;
+} 
+# }}} 
+
+# {{{ if it's not a text file, just stream it out.
+
+else { # file is binary, damn it
+       my $mime_type;
+       if ( $mime_type=
+            eval{ use MIME::Types;
+                  my($type,$encoding)=MIME::Types::by_suffix($path);
+                  $type; }) {
+           print $q->header($mime_type);       
+           $path=~s/[\|\>\<\&]//g;
+           open F,"$RT::MasonComponentRoot/$path" or 
+             die "couldn't open $path -- $!";
+           print while <F>; 
+           close F;
+       } else {
+           die "couldn't resolve type of non-text file (!@; $type) -- install Mime::Types\n";
+       }
+    }
+
+# }}}
+
+untie %HTML::Mason::Commands::session;
diff --git a/rt/bin/rt b/rt/bin/rt
new file mode 100755 (executable)
index 0000000..41220bb
--- /dev/null
+++ b/rt/bin/rt
@@ -0,0 +1,1391 @@
+#!!!PERL!! -w
+#
+# $Header: /home/cvs/cvsroot/freeside/rt/bin/Attic/rt,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# RT is (c) 1996-2001 Jesse Vincent <jesse@bestpractical.com>
+
+use strict;
+use Carp;
+use Getopt::Long;
+
+use lib "!!RT_LIB_PATH!!";
+use lib "!!RT_ETC_PATH!!";
+
+use RT::Interface::CLI  qw(CleanEnv LoadConfig DBConnect 
+                          GetCurrentUser GetMessageContent);
+
+#Clean out all the nasties from the environment
+CleanEnv();
+
+#Load etc/config.pm and drop privs
+LoadConfig();
+
+#Connect to the database and get RT::SystemUser and RT::Nobody loaded
+DBConnect();
+
+#Drop setgid permissions
+RT::DropSetGIDPermissions();
+
+#Get the current user all loaded
+my $CurrentUser = GetCurrentUser();
+
+unless ($CurrentUser->Id) {
+       print "No RT user found. Please consult your RT administrator.\n";
+       exit(1);
+}
+
+
+# {{{ commandline flags 
+
+my ( @id,
+     @limit_queue,
+     @limit_status,
+     @limit_owner,
+     @limit_priority,
+     @limit_final_priority,
+     @limit_requestor,
+     @limit_subject,
+     @limit_body,
+     @limit_created,
+     @limit_resolved,
+     @limit_lastupdated,
+     @limit_dependson,
+     @limit_dependedonby,
+     @limit_memberof,
+     @limit_hasmember,
+     @limit_refersto,
+     @limit_referredtoby,
+     @limit_keyword,
+     
+     @limit_due,
+     @limit_starts,
+     @limit_started,
+     $limit_first,
+     $limit_rows,
+     $history,
+     $summary,
+     $create,
+     @requestors,
+     @cc,
+     @admincc,
+     $status,
+     $subject,
+     $owner,
+     $steal,
+     $queue,
+     $time_left,
+     $priority,
+     $final_priority,
+     $due,
+     $starts,
+     $started,
+     $contacted,
+     $comment,
+     $reply,
+     $source,
+     $edit,
+     @dependson,
+     @memberof, 
+     @refersto,
+     $mergeinto,
+     @keywords,
+     $time_taken,
+     $verbose,
+     $debug,
+   $help,
+   $version);
+
+# }}}
+
+# Set defaults for cli args
+
+$edit = 1; # Assume the user wants to edit replies and comments 
+           # unless they specify --noedit
+
+# {{{    args
+
+my @args =("id=s" => \@id,
+          "limit-queue=s" => \@limit_queue,
+          "limit-status=s" => \@limit_status,
+          "limit-owner=s" => \@limit_owner,
+          "limit-priority=s" => \@limit_priority,
+          "limit-final-priority=s" => \@limit_final_priority,
+          "limit-requestor=s" => \@limit_requestor,
+          "limit-subject=s" => \@limit_subject,
+          "limit-body=s",      \@limit_body,
+          "limit-created=s" => \@limit_created,
+          "limit-due=s" =>     \@limit_due,
+          "limit-last-updated=s" => \@limit_lastupdated,
+          "limit-keyword=s" => \@limit_keyword,
+
+          "limit-member-of=s" => \@limit_memberof,
+          "limit-has-member=s" => \@limit_hasmember,
+          "limit-depended-on-by=s" => \@limit_dependedonby,
+          "limit-depends-on=s" => \@limit_dependson,
+          "limit-referred-to-by=s" => \@limit_referredtoby,
+          "limit-refers-to=s" => \@limit_refersto,
+
+          "limit-starts=s" => \@limit_starts,
+          "limit-started=s" => \@limit_started,
+          "limit-first=i" => \$limit_first,
+          "limit-rows=i" => \$limit_rows,
+          "history|show" => \$history,
+          "summary:s" => \$summary,
+          "create" => \$create,
+          "keywords=s" => \@keywords,
+          "requestor|requestors=s" => \@requestors,
+          "cc=s" => \@cc,
+          "admincc=s" => \@admincc,
+          "status=s" => \$status,
+          "subject=s" => \$subject,
+          "owner=s" => \$owner,
+          "steal" => \$steal,
+          "queue=s" => \$queue,
+
+          
+          "priority=i" => \$priority,
+          "final-priority=i" => \$final_priority,
+          "due=s" => \$due,
+          "starts=s" => \$starts,
+          "started=s" => \$started,
+          "contacted=s" => \$contacted,
+          "comment", \$comment,
+          "reply|respond", \$reply,
+          "source=s" => \$source,
+          "edit!" => \$edit,
+          "depends-on=s" => \@dependson,
+          "member-of=s" => \@memberof, 
+          "merge-into=s" => \$mergeinto, 
+          "refers-to=s" => \@refersto,
+          "time-left=i" => \$time_left,
+          "time-taken=i" => \$time_taken,
+          "verbose+" => \$verbose,
+          "debug" => \$debug,
+          "version" => \$version,
+          "help|h|usage" => \$help
+         );
+
+# }}}
+
+
+
+GetOptions(@args);
+
+print join(':',@keywords);
+# {{{ If they want it, print a usage message and get out
+
+if ($help) {
+
+
+print <<EOUSAGE;
+
+Limit the set of records returned:
+
+--id=[first][-][last]
+  Specify a single ticket, a range, or to start with (n-) or end with (-n)
+a specific ticket.
+  
+  --limit-queue=<queue>
+         --limit-status=[!](new|open|stalled|resolved)
+
+         --limit-owner=[!]<userid>
+         --limit-priority=[starts][-][ends]
+         --limit-final-priority=[starts][-][ends]
+           starts is less than ends
+         --limit-requestor=[!]<userid>|<email>
+         --limit-subject=[!]<text>
+         --limit-body=[!]<text>
+         --limit-keyword=[!]<select>/<keyword>
+        
+       Links
+          --limit-member-of=<ticketid>
+          --limit-has-member=<ticketid>
+          --limit-refers-to=<ticketid>
+          --limit-referred-to-by=<ticketid>
+          --limit-depends-on=<ticketid>
+          --limit-depended-on-by=<ticketid>
+
+
+       Dates
+         --limit-created=[starts][-][ends]
+         --limit-due=[starts][-][ends]
+         --limit-starts=[starts][-][ends]
+         --limit-started=[starts][-][ends]
+          --limit-resolved=[starts][-][ends]
+          --limit-last-updated=[starts][-][ends]
+           starts and ends are dates.  starts can not be less than ends
+
+         --limit-first=<first row returned>
+         --limit-rows=<row count>
+
+         --history | --show
+            show a history of the tickets found
+
+         --summary [format-string]
+             show a listing-style summary of the tickets found. If format string
+             is ommitted, uses \$RT_SUMMARY_FORMAT or an internal default
+            
+
+             #TODO: doc summary 
+             format: <atom>%<format>
+             atom:   <name><size>
+             size: <integer>
+             name:  (grep for # {{{ attribs for the array of ok values)
+
+
+         --create
+            create a new ticket. Any attributes that you can modify on an existing ticket
+            can also be used for ticket creation.
+
+
+
+Attributes
+  Basics
+         --status=<new|open|stalled|resolved|dead>
+           sets status
+          --subject=<subject>
+           sets subject
+          --owner=<userid>
+           set owner to 
+           --steal
+           Become the owner, even if someone else owns the ticket
+          --queue=<queueid>
+           set queue to
+          
+          --priority=<int>
+         
+           --final-priority=<int>
+
+  Watchers
+         --requestors=[+|-]<userid|email address>
+          add or remove this user as a ticket requestor 
+         --cc=[+|-]<userid|email address>
+          add or remove this user as a ticket cc
+         --admincc=[+|-]<userid|email address>
+          add or remove this user as a ticket admincc
+
+       (When creating tickets, just leave off the + or - )
+
+  Keywords
+         --keywords[+|-]<keyword_select>/<keyword>
+          Add or remove a keyword.
+
+
+
+  Dates
+          --due=<date>
+          --starts=<date>
+          --started=<date>
+          --contacted=<date>
+
+          --time-left=<int>
+            
+          --time-taken=<int>
+
+
+   Link related manipulation:
+
+          --depends-on=[+|-]<ticketid>
+          --member-of=[+|-]<ticketid>
+          --refers-to=[+|-]<ticketid>
+           --merge-into=<ticketid>
+
+Comments and replies
+
+          --comment
+          --reply|respond
+            --source <path>
+                Specify the path to the source file for this ticket update
+
+             --noedit
+                Don't invoke \$EDITOR to edit the content of this update
+
+
+
+
+   Condiments
+
+          --verbose
+          --debug
+          --version
+          --help|h|usage
+             You're reading it.
+
+EOUSAGE
+
+    exit(0);
+}
+
+# Print version, and leave
+if ($version) {
+       print "RT $RT::VERSION for $RT::rtname. Copyright 1996-2001 Jesse Vincent <jesse\@fsck.com>\n";
+       exit(0);
+}
+
+# }}}
+
+# {{{ Validate any options that were passed in. normalize them.
+
+#if a queue was specified
+if ($queue) {
+    # make sure that $queue is a valid queue and load it into $queue_obj
+}
+
+#For each date in: $due, $starts, $started
+
+# load up an RT::Date object and parse it into a normalized form
+# if it can't parse it, log an error and null out the variable
+
+# }}}
+
+# {{{ Check if we're creating, if so, create the ticket and be done
+
+if ($create) {
+    $RT::Logger->debug("Creating a new ticket");
+
+    #Make sure the current user can create tickets in this queue
+    
+    #Make sure that the owner specified can own tickets in this queue
+
+
+           
+    my $linesref = GetMessageContent( Edit => $edit, Source => $source,
+                                     CurrentUser => $CurrentUser
+                                   );
+    
+    require MIME::Entity;
+    my $MIMEObj;
+    
+    if ($linesref) {
+       $MIMEObj = MIME::Entity->build(Data => $linesref);
+    }  
+    
+    use RT::Ticket;
+    my $Ticket=new RT::Ticket($CurrentUser);
+    my ($ticket, $trans, $msg) =
+      $Ticket->Create(Queue => $queue,
+                     Owner => $owner,
+                     Status => $status || 'new' ,
+                     Subject => $subject,
+                     Requestor => \@requestors,
+                     Cc => \@cc,
+                     AdminCc => \@admincc,
+                     Due => $due,
+                     Starts => $starts,
+                     Started => $started,
+                     TimeLeft => $time_left,
+                     InitialPriority => $priority,
+                     FinalPriority => $final_priority,
+                     MIMEObj => $MIMEObj
+                    );
+    print $msg . "\n";
+}
+
+# }}}
+
+else {
+    #Apply restrictions
+    use RT::Tickets;
+    my $Tickets = new RT::Tickets($CurrentUser);
+    
+    # {{{ Limit our search
+    my $value;                 #to use when iterating through restrictions
+    my $queue_id;              #to use when limiting by keyword
+    
+    # {{{ limit on id
+
+    foreach $value (@id) {
+       if ($value =~ /^(\d+)$/) {
+           $Tickets->LimitId ( VALUE => $1,
+                               OPERATOR => '=');
+       }       
+       elsif ($value =~ /^(\d*)\D?(\d*)$/) {
+           my $start = $1;
+           my $end = $2;
+           $Tickets->LimitId(
+                             VALUE => "$start",
+                             OPERATOR => '>=') if ($start);
+           $Tickets->LimitId(
+                             VALUE => "$end",
+                             OPERATOR => '<=') if ($end);
+       }       
+    }
+
+
+    # }}}
+    
+    # {{{ limit on status
+
+    foreach $value (@limit_status) {
+       if ($value =~ /^(=|!=|!|)(.*)$/) {
+           my $op = $1;
+           my $val = $2;
+                
+
+           $op = ParseBooleanOp($op);
+           $Tickets->LimitStatus(VALUE => "$val",
+                                 OPERATOR => "$op");
+       }       
+    }
+
+    # }}}
+
+
+
+    # {{{ limit on queue
+    foreach $value (@limit_queue) {
+       if ($value =~ /^(\W?)(.*?)$/i) {
+           my $op = $1;
+           my $val = $2;
+               
+           $op = ParseBooleanOp($op);
+
+           my $queue_obj = new RT::Queue($RT::SystemUser);
+               
+           unless ($queue_obj->Load($val)) {
+               $RT::Logger->debug("Queue '$val' not found");
+               print STDERR "Queue '$val' not found\n";        
+               exit(-1);
+           }
+           $RT::Logger->debug ("Limiting queue to $op ".$queue_obj->Name);
+           $Tickets->LimitQueue(VALUE => $queue_obj->Name,
+                                OPERATOR => $op);
+           $queue_id=$queue_obj->id;
+       }       
+    }  
+
+    # {{{ limit on keyword
+    foreach $value (@limit_keyword) {
+       if ($value =~ /^(\W?)(.*?)\/(.*)$/i) {
+           my $op = $1;
+           my $select = $2;
+           my $keyword = $3;
+
+           $op = ParseBooleanOp($op);
+
+           # load the keyword select
+           my $keyselect = RT::KeywordSelect->new($RT::SystemUser);
+           unless ($keyselect->LoadByName(Name=>$select, Queue=>$queue_id)) {
+               $RT::Logger->debug("KeywordSelect '$select' not found");
+               print STDERR "KeywordSelect '$select' not fount\n";
+               exit(-1);
+           }
+
+           # load the keyword
+           my $k = RT::Keyword->new($RT::SystemUser);
+           unless ($k->LoadByNameAndParentId($keyword, $keyselect->Keyword)) {
+               $RT::Logger->debug("Keyword '$keyword' not found");
+               print STDERR "Keyword '$keyword' not found\n";
+               exit(-1);
+           }
+           $Tickets->LimitKeyword(OPERATOR => $op,
+                                  KEYWORDSELECT => $keyselect->id,
+                                  KEYWORD => $k->id);
+           $RT::Logger->debug ("Limiting keyword to $op ".$k->Path);
+       }
+    }
+    # }}}
+    # {{{ limit on owner
+    foreach $value (@limit_owner) {
+       if ($value =~ /^(\W?)(.*?)$/i) {
+           my $op = $1;
+           my $val = $2;
+               
+           $op = ParseBooleanOp($op);
+
+           my $user_obj = new RT::User($RT::SystemUser);
+               
+           unless ($user_obj->Load($val)) {
+               $RT::Logger->debug("User '$val' not found");
+               print STDERR "User '$val' not found\n"; 
+               exit(-1);
+           }
+           $val = $user_obj->id();
+               
+           $RT::Logger->debug ("Limiting owner to $op $val");
+           $Tickets->LimitOwner(VALUE => "$val",
+                                OPERATOR => "$op");
+       }       
+    }  
+    # }}}
+    # {{{ limt on priority
+
+    foreach $value (@limit_priority) {
+       my ($start, $end) = ParseRange($value);
+       if ($start == $end) {
+           $Tickets->LimitPriority( VALUE => $start,
+                                    OPERATOR => '=');
+       } elsif ($start) {
+           $Tickets->LimitPriority( VALUE => $start,
+                                    OPERATOR => '>=');
+       } elsif ($end) {
+           $Tickets->LimitPriority( VALUE => $end,
+                                    OPERATOR => '<=');
+       }       
+           
+    }
+    foreach $value (@limit_final_priority) {
+       my ($start, $end) = ParseRange($value);
+       if ($start == $end) {
+           $Tickets->LimitFinalPriority( VALUE => $start,
+                                         OPERATOR => '=');
+       } elsif ($start) {
+           $Tickets->LimitFinalPriority( VALUE => $start,
+                                         OPERATOR => '>=');
+       } elsif ($end) {
+           $Tickets->LimitFinalPriority( VALUE => $end,
+                                         OPERATOR => '<=');
+       }       
+    }
+    # }}}
+
+    foreach $value (@limit_requestor) {
+       if ($value =~ /^(\W?)(.*?)$/i) {
+           my $op = $1;
+           my $val = $2;
+               
+           $op = ParseBooleanOp($op);
+           $Tickets->LimitRequestor(VALUE => $val,
+                                    OPERATOR => $op );
+       }
+           
+    }
+    foreach $value (@limit_subject) {
+       
+       if ($value =~ /^(\W?)(.*?)$/i) {
+           my $op = $1;
+           my $val = $2;
+           
+           $op = ParseLikeOp($op);
+           
+           $Tickets->LimitSubject(VALUE => $val,
+                                  OPERATOR => $op );
+           }
+    }
+    
+    foreach $value (@limit_body) {
+       if ($value =~ /^(\W?)(.*?)$/i) {
+           my $op = $1;
+           my $val = $2;
+           
+           $op = ParseLikeOp($op);
+           
+               $Tickets->LimitBody(VALUE => $val,
+                                   OPERATOR => $op );
+       }       
+       
+    }
+    
+    
+    
+    # Dates
+    foreach my $date (@limit_created) {
+       my ($start, $end) = ParseDateRange($date);
+       $Tickets->LimitCreated ( VALUE => $start,
+                                OPERATOR => '>=' ) if ($start);
+       $Tickets->LimitCreated ( VALUE => $end,
+                                OPERATOR => '<=' ) if ($end);
+    }
+
+    foreach my $date (@limit_due) {
+       my ($start, $end) = ParseDateRange($date);
+       $Tickets->LimitDue ( VALUE => $start,
+                                OPERATOR => '>=' ) if ($start);
+       $Tickets->LimitDue ( VALUE => $end,
+                                OPERATOR => '<=' ) if ($end);
+    }
+
+    foreach my $date (@limit_starts) {
+       my ($start, $end) = ParseDateRange($date);
+       $Tickets->LimitStarts ( VALUE => $start,
+                                OPERATOR => '>=' ) if ($start);
+       $Tickets->LimitStarts ( VALUE => $end,
+                                OPERATOR => '<=' ) if ($end);
+    }
+
+    foreach my $date (@limit_started) {
+       my ($start, $end) = ParseDateRange($date);
+       $Tickets->LimitStarted ( VALUE => $start,
+                                OPERATOR => '>=' ) if ($start);
+       $Tickets->LimitStarted ( VALUE => $end,
+                                OPERATOR => '<=' ) if ($end);
+    }
+
+    foreach my $date (@limit_resolved) {
+       my ($start, $end) = ParseDateRange($date);
+       $Tickets->LimitResolved ( VALUE => $start,
+                                OPERATOR => '>=' ) if ($start);
+       $Tickets->LimitResolved ( VALUE => $end,
+                                OPERATOR => '<=' ) if ($end);
+    }
+
+    foreach my $date (@limit_lastupdated) {
+       my ($start, $end) = ParseDateRange($date);
+       $Tickets->LimitLastUpdated( VALUE => $start,
+                                OPERATOR => '>=' ) if ($start);
+       $Tickets->LimitLastUpdated ( VALUE => $end,
+                                OPERATOR => '<=' ) if ($end);
+    }
+
+    foreach my $link (@limit_memberof) {
+       $Tickets->LimitMemberOf($link);
+    }  
+
+    foreach my $link (@limit_hasmember) {
+       $Tickets->LimitHasMember($link);
+    }  
+
+    foreach my $link (@limit_dependson) {
+       $Tickets->LimitDependsOn($link);
+    }  
+
+    foreach my $link (@limit_dependedonby) {
+       $Tickets->LimitDependedOnBy($link);
+    }
+    foreach my $link (@limit_refersto) {
+       $Tickets->LimitRefersTo($link);
+    }  
+    
+    foreach my $link (@limit_referredtoby) {
+       $Tickets->LimitReferredToBy($link);
+    }  
+
+    
+    if ($limit_first){
+    }
+    if ($limit_rows){
+    }
+
+# }}}
+    
+    # {{{ Iterate through all tickets we found
+
+
+    my ($format, $titles, $code);
+    
+    #Set up the summary format if we need to
+    if (defined $summary) {
+       my $format_string = $summary || $ENV{'RT_SUMMARY_FORMAT'} || "%id4%status4%queue7%subject40%requestor16";
+
+       ($format, $titles, $code) = BuildListingFormat($format_string);
+        printf "$format\n", eval "$titles";
+   }   
+
+
+    while (my $Ticket = $Tickets->Next()) {
+       $RT::Logger->debug ("Now working on ticket ". $Ticket->id);
+    
+       #Run through all the ticket modifications we might want to do
+       #TODO: these are all insufficiently lazy and should be replaced with some 
+       # nice foreaches.
+
+
+       # {{{ deal with watchers
+       
+       # add / delete requestors
+       foreach $value (@requestors) {
+           if ($value =~ /^(\W?)(.*)$/) {
+               my $op = $1;
+               my $addr = $2;
+               
+               $Ticket->AddRequestor(Email => $addr) if ($op eq '+');
+               $Ticket->DeleteRequestor( $addr) if ($op eq '-');
+           }   
+       }
+       
+       # add / delete ccs
+       foreach $value (@cc) {
+           if ($value =~ /^(\W?)(.*)$/) {
+               my $op = $1;
+               my $addr = $2;
+               $Ticket->AddCc(Email => $addr) if ($op eq '+');
+               $Ticket->DeleteCc($addr) if ($op eq '-');
+           }   
+       }       
+       
+       # add / delete adminccs
+        $RT::Logger->debug("Looking at admin ccs");
+       foreach $value (@admincc) {
+           if ($value =~ /^(\W?)(.*)$/) {
+               my $op = $1;
+               my $addr = $2;
+               $Ticket->AddAdminCc(Email => $addr) if ($op eq '+');
+               $Ticket->DeleteAdminCc($addr) if ($op eq '-');
+           }   
+       }       
+
+       # }}}
+       
+       # {{{ Deal with ticket keywords
+
+       my $KeywordSelects = $Ticket->QueueObj->KeywordSelects();
+        $RT::Logger->debug ("Looking at keywords");
+       foreach $value (@keywords) {
+           $RT::Logger->debug("Looking at --keyword=$value");
+           if ($value =~ /^(\W?)(.*?)\/(.*)$/) {
+               my $op = $1;
+               my $select = $2;
+               my $keyword = $3;
+               
+               $RT::Logger->debug("Going to $op Keyword $select / $keyword");  
+               while (my $ks = $KeywordSelects->Next) {
+                    $RT::Logger->debug("$select is select ".$ks->Name." is found");
+                   next unless ($ks->Name =~ /$select/i);
+                   $RT::Logger->debug ("Found a match for $select\n"); 
+                   my $kids = $ks->KeywordObj->Descendents;
+    
+                    my ($kid);
+                   foreach $kid (keys %{$kids}) {
+                        $RT::Logger->debug("Now comparing $keyword with ".$kids->{$kid}. "\n");
+                       next unless ($kids->{$kid} =~ /^$keyword$/i);
+                       $RT::Logger->debug("Going to $op $select / $keyword (".$kids->{$kid} .")");     
+                       $Ticket->DeleteKeyword(KeywordSelect => $ks->id,
+                                           Keyword => $kid) if ($op eq '-');
+                       
+                       $Ticket->AddKeyword(KeywordSelect => $ks->id,
+                                           Keyword => $kid) if ($op eq '+');
+                   }
+                   
+               }
+           }
+       }
+       # }}}
+       
+       # {{{ deal with links
+
+       # Deal with merging {
+       if ($mergeinto) {
+               my ($trans, $msg) =$Ticket->MergeInto($mergeinto);
+               print $msg."\n";
+       }       
+       # add /delete depends-ons
+
+       foreach my $value (@dependson) {
+           if ($value =~ /^(\W?)(.*)$/) {
+               my $op = $1;
+               my $ticket = $2;
+               if (!$op or ($op eq '+')) {
+                   my ($trans, $msg) =
+                     $Ticket->AddLink(Type => 'DependsOn', Target => $ticket);
+                   print $msg."\n";
+               }
+               elsif ($op eq '-') {
+                   my ($trans, $msg) = 
+                     $Ticket->DeleteLink(Type => 'DependsOn', Target => $ticket);
+                   print $msg."\n";
+               }
+
+           }
+       }
+       # add /delete member-of
+       foreach my $value (@memberof) {
+           if ($value =~ /^(\W?)(.*)$/) {
+               my $op = $1;
+               my $ticket = $2;
+               if ($op eq '+') {
+                   my ($trans, $msg) =
+                     $Ticket->AddLink(Type => 'MemberOf', Target => $ticket);
+                   print $msg;
+               }
+               elsif ($op eq '-') {
+                   my ($trans, $msg) = 
+                     $Ticket->DeleteLink(Type => 'MemberOf', Target => $ticket);
+                   print $msg;
+               }
+
+           }
+       }       
+       # add / delete refers-to
+               foreach my $value (@refersto) {
+           if ($value =~ /^(\W?)(.*)$/) {
+               my $op = $1;
+               my $ticket = $2;
+               if ($op eq '+') {
+                   my ($trans, $msg) =
+                     $Ticket->AddLink(Type => 'RefersTo', Target => $ticket);
+                   print $msg;
+               }
+               elsif ($op eq '-') {
+                   my ($trans, $msg) = 
+                     $Ticket->DeleteLink(Type => 'RefersTo', Target => $ticket);
+                   print $msg;
+               }
+
+           }
+       }
+
+       # }}}
+       
+       # {{{ deal with dates
+       
+       #set due 
+       if ($due) {
+           my $iso = ParseDateToISO($due);
+           if ($iso) {
+               $RT::Logger->debug("Setting due date to $iso ($due)");
+               my ($trans, $msg) = 
+                 $Ticket->SetDue($iso);
+               print $msg;
+           }
+           else {
+               print "Due date '$due' could not be parsed";
+           }
+       }
+
+       #set starts
+       if ($starts) {
+           my $iso = ParseDateToISO($due);
+           if ($iso) {
+               my ($trans, $msg) = 
+                 $Ticket->SetStarts($iso);
+               print $msg."\n";
+           }
+           else {
+               print "Starts date '$starts' could not be parsed";
+           }
+       }
+       #set started
+               if ($started) {
+           my $iso = ParseDateToISO($started);
+           if ($iso) {
+               my ($trans, $msg) = 
+                 $Ticket->SetStarted($iso);
+               print $msg."\n";
+           }
+           else {
+               print "Started date '$started' could not be parsed";
+           }
+       }
+       #set contacted
+               if ($contacted) {
+           my $iso = ParseDateToISO($contacted);
+           if ($iso) {
+               my ($trans, $msg) = 
+                 $Ticket->SetContacted($iso);
+               print $msg."\n";
+           }
+           else {
+               print "Contacted date '$contacted' could not be parsed";
+           }
+       }
+
+    # }}}
+       
+       # {{{ set other attributes
+
+       #Set subject
+       if ($subject) {
+           my ($trans, $msg) = $Ticket->SetSubject($subject);
+           print $msg."\n";
+       }
+       
+       #Set priority
+       if ($priority) {
+           my ($trans, $msg) = 
+             $Ticket->SetPriority($priority);
+           print $msg."\n";
+       }
+       
+       #Set final priority
+       if ($final_priority) {
+           my ($trans, $msg) =
+             $Ticket->SetFinalPriority($final_priority);
+           print $msg."\n";
+       }
+
+       #Set status
+       if ($status) {
+           my ($trans, $msg) = 
+             $Ticket->SetStatus($status);
+           print $msg."\n";
+       }
+       
+       #Set time left
+       if ($time_left) {
+           my ($trans, $msg) = 
+             $Ticket->SetTimeLeft($time_left);
+           print $msg."\n";
+       }
+
+       #Set time_taken 
+       if ($time_taken) {
+           my ($trans, $msg) = 
+             $Ticket->SetTimeTaken($time_taken);
+           print $msg."\n";
+       }
+       
+       #Set owner
+       if ($owner) {
+           my ($trans, $msg) =
+             $Ticket->SetOwner($owner);
+           print $msg."\n";
+       }
+
+        # Steal
+        if ($steal) {
+                my ($trans, $msg) =
+                 $Ticket->Steal();
+                 print $msg . "\n";
+        }
+       #Set queue 
+       if ($queue) {
+           my ($trans, $msg) = 
+             $Ticket->SetQueue($queue);
+           print $msg."\n";
+       }
+
+    # }}}
+       
+
+
+       # {{{ Perform ticket comments/replies
+       if ($reply) {
+           $RT::Logger->debug("Replying to ticket ".$Ticket->Id);
+           
+           my $linesref = GetMessageContent( Edit => $edit, Source => $source,
+                                            CurrentUser => $CurrentUser
+                                          );
+           
+           #TODO build this entity
+           require MIME::Entity;
+           my $MIMEObj = MIME::Entity->build(Data => $linesref);
+           
+           $Ticket->Correspond( MIMEObj => $MIMEObj ,
+                                TimeTaken => $time_taken);
+       }       
+       
+       elsif ($comment) {
+           $RT::Logger->debug("Commenting on ticket ".$Ticket->Id);
+       
+           my $linesref =GetMessageContent(Edit => $edit, Source => $source,
+                                           CurrentUser => $CurrentUser);
+           #TODO build this entity
+           require MIME::Entity;
+           my $MIMEObj = MIME::Entity->build(Data => $linesref);
+           
+           $Ticket->Comment( MIMEObj => $MIMEObj,
+                             TimeTaken => $time_taken);
+       }
+
+    # }}}
+       
+       # {{{ Display whatever we need to display
+
+       # {{{ Display a full ticket listing and history
+       if ($history) {
+           #Display the history
+           $RT::Logger->debug("Show history for ".$Ticket->id);
+           
+           if ($Ticket->CurrentUserHasRight("ShowTicket")) {
+               &ShowSummary($Ticket);
+               print "\n";
+               &ShowHistory($Ticket);
+           }
+           else {
+               print "You don't have permission to view that ticket.\n";
+           }
+       }       
+
+       # }}}
+       
+       # {{{ Display a summary if we need to
+       if (defined $summary) {
+           $RT::Logger->debug ("Show ticket summary with format $format");
+           
+           printf $format."\n", eval $code;
+           
+       }       
+       # }}}
+
+       # }}}
+       
+    }
+
+    # }}}
+    
+}
+
+
+$RT::Handle->Disconnect();
+
+
+
+
+
+
+
+# {{{ sub ParseBooleanOp
+
+=head2 ParseBooleanOp
+
+  Takes an option modifier. returns the apropriate SQL operator.
+  If it's handed ! or -, returns !=.  Otherwise returns =.
+
+=cut
+
+sub ParseBooleanOp {
+    
+    my $op = shift;
+    
+    #so that !new limits to not new, etc
+    if ($op =~ /^(\!|-)/) {
+       $op = "!=";
+    }
+    else {
+       $op = "=";
+    }
+    
+    return($op);
+}
+
+# }}}
+
+# {{{ sub ParseLikeOp
+=head2 ParseLikeOp
+
+  Takes an option modifier. returns the apropriate SQL operator.
+  If it's handed ! or -, returns NOT  LIKE.  Otherwise returns LIKE
+
+=cut
+
+sub ParseLikeOp {
+    
+    my $op = shift;
+    
+    #so that !new limits to not new, etc
+    if ($op =~ /^(\!|-)/) {
+       $op = "NOT LIKE";
+    }
+    else {
+       $op = "LIKE";
+    }
+    
+    return($op);
+}
+# }}}
+
+# {{{ sub ParseDateToISO
+
+=head2 ParseDateToISO
+
+Takes a date in an arbitrary format.
+Returns an ISO date and time in GMT
+
+=cut
+
+sub ParseDateToISO {
+    my $date = shift;
+
+       my $date_obj = new RT::Date($CurrentUser);
+       $date_obj->Set( Format => 'unknown',
+                       Value => $date
+                     );
+       return ($date_obj->ISO);
+}
+
+# }}}
+
+# {{{ sub ParseDateRange
+
+=head2 ParseDateRange [RANGE]
+
+Takes a range of dates of the form [<date>][-][<date>] and returns 
+starting and ending dates (as ISOs) If a date is specified as neither a starting nor ending 
+date, we parse it it as "midnight tonight to midnight tomorrow"
+
+=cut
+
+sub ParseDateRange {
+    my $in = shift;
+    my ($start, $end);
+    
+    
+    use RT::Date;
+    my $start_obj = new RT::Date($CurrentUser);
+    my $end_obj = new RT::Date($CurrentUser);
+    
+    if ($in =~ /^(.*?)-(.*?)$/) {
+       $start = $1;
+       $end = $2;
+
+       if ($start) {
+           $start_obj->Set(Format => 'unknown', 
+                           Value => $start);
+       }
+       if ($end) {
+           $end_obj->Set(Format => 'unknown', 
+                         Value => $end);
+       }
+    }
+    else {
+       $start = $in;
+       $end = $in;
+
+       $start_obj->Set(Format => 'unknown', 
+                       Value => $start);
+       
+       $end_obj->Set(Format => 'unknown', 
+                     Value => $end);
+       
+       $start_obj->SetToMidnight();
+       $end_obj->SetToMidnight();
+       $end_obj->AddDay();
+    }  
+    
+    if ($start) {
+       $start = $start_obj->ISO;
+    }
+    if ($end) {
+       $end = $end_obj->ISO;
+    }
+
+    return ($start, $end);
+}
+
+# }}}
+
+# {{{ ParseRange
+=head2 ParseRange [RANGE]
+
+Takes a range of the form [<int>][-][<int>] and returns 
+a first and a last value. If the - is omitted, both $start and $end are the same.
+=cut
+
+sub ParseRange {
+    my $in = shift;
+    my ($start, $end);
+    
+    if ($in =~ /(.*?)-(.*?)/) {
+       $start = $1;
+       $end = $2;
+    }
+    else {
+       $start = $in;
+       $end = $in;
+    }  
+    
+    return ($start, $end);
+    
+
+    
+}
+
+# }}}
+         
+# {{{ sub ShowSummary 
+
+sub ShowSummary  {
+    my $Ticket = shift;
+
+
+    print <<EOFORM;
+Serial Number: @{[$Ticket->Id]}   Status:@{[$Ticket->Status]} Worked: @{[$Ticket->TimeWorked]} minutes  Queue:@{[$Ticket->QueueObj->Name]}
+      Subject: @{[$Ticket->Subject]}
+   Requestors: @{[$Ticket->RequestorsAsString]}
+           Cc: @{[$Ticket->CcAsString]}
+     Admin Cc: @{[$Ticket->AdminCcAsString]}
+        Owner: @{[$Ticket->OwnerObj->Name]}
+     Priority: @{[$Ticket->Priority]} / @{[$Ticket->FinalPriority]}
+          Due: @{[$Ticket->DueAsString]}
+      Created: @{[$Ticket->CreatedAsString]} (@{[$Ticket->AgeAsString]})
+ Last Contact: @{[$Ticket->ToldAsString]} (@{[$Ticket->LongSinceToldAsString]})
+  Last Update: @{[$Ticket->LastUpdatedAsString]} by @{[$Ticket->LastUpdatedByObj->Name]}
+                
+EOFORM
+
+my $selects = $Ticket->QueueObj->KeywordSelects();
+    #get the keyword selects
+    print "Keywords:\n";
+    while (my $select = $selects->Next) {
+       print "\t" .$select->Name .": ";
+       my $keys = $Ticket->KeywordsObj($select->id);   
+       while (my $key = $keys->Next) {
+           print $key->KeywordObj->RelativePath($select->KeywordObj) . "  ";
+           
+       }       
+       print "\n";
+    }
+    
+#iterate through the keyword selects.
+#print the keyword select and all the related keywords
+
+
+
+#TODO: finish link  descriptions
+print "Dependencies: \n";
+   while (my $l=$Ticket->DependedOnBy->Next) {
+       print $l->BaseObj->id," (",$l->BaseObj->Subject,") ",$l->Type," this ticket\n";
+   }
+   while (my $l=$Ticket->DependsOn->Next) {
+       print "This ticket ",$l->Type," ",$l->TargetObj->Id," (",$l->TargetObj->Subject,")\n";
+   }
+}
+
+# }}}
+
+# {{{ sub ShowHistory 
+sub ShowHistory  {
+    my $Ticket = shift;
+    my $Transaction;    
+    my $Transactions = $Ticket->Transactions;
+
+    while ($Transaction = $Transactions->Next) {
+      &ShowTransaction($Transaction);
+    }   
+  }
+# }}}
+
+# {{{ sub ShowTransaction 
+sub ShowTransaction  {
+  my $transaction = shift;
+  
+print <<EOFORM;
+==========================================================================
+Date: @{[$transaction->CreatedAsString]} (@{[$transaction->TimeTaken]} minutes)
+@{[$transaction->Description]}
+EOFORM
+    ;
+  my $attachments=$transaction->Attachments();
+  while (my $message=$attachments->Next) {
+    print <<EOFORM;
+--------------------------------------------------------------------------
+@{[$message->Headers]}
+EOFORM
+
+    if ($message->ContentType =~ m{^(text/plain|message|text$)}) {
+       print $message->Content;
+    } else {
+       print $message->ContentType, " not shown";
+    }
+  }
+  print "\n";
+  return();
+}
+# }}}
+
+
+# {{{ sub BuildListingFormat
+
+sub BuildListingFormat {
+    my $format_string = shift;
+
+    my ($id, @format, @code, @titles);
+    my ($field,$titles,$length, $format);
+
+    my $code = "";
+
+    # {{{ attribs
+    my $attribs = { id => { chars => '4',
+                           justify => 'r',
+                           title => 'id',
+                           value => '$Ticket->id',
+                         },
+                   
+                   queue => { chars => '8',
+                              justify => 'l',
+                              title => 'Queue',
+                              value => '$Ticket->QueueObj->Name' 
+                            },
+                   subject => { chars => '30',
+                                justify => 'l',
+                                title => 'Subject',
+                                value => '$Ticket->Subject',
+                              },
+                   priority => { chars => '2',
+                                 justify => 'r',
+                                 title => 'Pri',
+                                 value => '$Ticket->Priority',
+                               },
+                   final_priority => {  chars => '2',
+                                        justify => 'r',
+                                        title => 'Fin',
+                                        value => '$Ticket->FinalPriority',
+                                     },
+                   time_worked => { chars => '6',
+                                    justify => 'r',
+                                    title => 'Worked',
+                                    value => '$Ticket->TimeWorked',
+                                  },
+                   time_left => { chars => '5',
+                                  justify => 'r',
+                                  title => 'Left',
+                                  value => '$Ticket->TimeLeft',
+                              
+                                },
+               
+                   status => {  chars => '6',
+                                justify => 'r',
+                                title => 'Status',
+                                value => '$Ticket->Status',
+                             },
+                   owner => {  chars => '10',
+                               justify => 'r',
+                               title => 'Owner',
+                               value => '$Ticket->OwnerObj->Name'
+                            },
+                   requestor => {  chars => '10',
+                                   justify => 'r',
+                                   title => 'Requestor',
+                                   value => '$Ticket->RequestorsAsString'
+                                },
+                   created => {  chars => '12',
+                                 justify => 'r',
+                                 title => 'Created',
+                                 value => '$Ticket->CreatedAsString'
+                              },
+                   updated => {  chars => '12',
+                                 justify => 'r',
+                                 title => 'Updated',
+                                 value => '$Ticket->LastUpdatedAsString'
+                              },
+                   due => {  chars => '12',
+                             justify => 'r',
+                             title => 'Due',
+                             value => '$Ticket->DueAsString'
+                          },
+                   told => {  chars => '12',
+                              justify => 'r',
+                              title => 'Told',
+                              value => '$Ticket->ToldAsString'
+                           },
+               
+               
+               
+                 };
+
+    # }}}
+    
+
+    foreach $field (split ('%',$format_string)) {
+       
+       if ($field =~ /^(\D*?)(\d*?)$/) {
+           $id = $1;
+           $length = $2;
+       }
+       else {  
+           $RT::Logger->debug ("Error parsing $field\n");
+       }
+       if ($length) {
+           push (@format, "%".$length.".".$length."s ");
+           
+           push (@code,  $attribs->{"$id"}->{'value'});
+                 
+           push (@titles, "'". $attribs->{"$id"}->{title}. "'");
+       }
+       
+       
+    }
+     $code = join (',', @code);
+     $format = join (" ", @format);
+     $titles = join (', ', @titles);
+    
+  
+    return ($format, $titles, $code);
+}
+
+# }}}
+
+
+
+1;
diff --git a/rt/bin/rt-mailgate b/rt/bin/rt-mailgate
new file mode 100755 (executable)
index 0000000..e6f0d95
--- /dev/null
@@ -0,0 +1,367 @@
+#!!!PERL!! -w
+
+# $Header: /home/cvs/cvsroot/freeside/rt/bin/rt-mailgate,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-2001 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+
+
+package RT;
+use strict;
+use vars qw($VERSION $Handle $Nobody $SystemUser);
+
+$VERSION="!!RT_VERSION!!";
+
+
+use lib "!!RT_LIB_PATH!!";
+use lib "!!RT_ETC_PATH!!";
+
+use RT::Interface::Email  qw(CleanEnv LoadConfig DBConnect
+                            GetCurrentUser
+                            GetMessageContent
+                            CheckForLoops 
+                            CheckForSuspiciousSender
+                            CheckForAutoGenerated 
+                            ParseMIMEEntityFromSTDIN
+                            ParseTicketId 
+                            MailError 
+                            ParseCcAddressesFromHead
+                            ParseSenderAddressFromHead 
+                            ParseErrorsToAddressFromHead
+                           );
+
+#Clean out all the nasties from the environment
+CleanEnv();
+
+#Load etc/config.pm and drop privs
+LoadConfig();
+
+#Connect to the database and get RT::SystemUser and RT::Nobody loaded
+DBConnect();
+
+#Drop setgid permissions
+RT::DropSetGIDPermissions();
+
+use RT::Ticket;
+use RT::Queue;
+use MIME::Parser;
+use File::Temp;
+use Mail::Address;
+
+
+#Set some sensible defaults 
+my $Queue = 1;
+my $time = time;
+my $Action = "correspond";  
+
+my ($Verbose, $ReturnTid, $Debug);
+my ($From, $TicketId, $Subject,$SquelchReplies);
+
+# using --owner-from-extension, this will let you set ticket owner on create
+my $AssignTicketTo = undef;
+my ($status, $msg);
+
+# {{{ parse commandline 
+
+while (my $flag = shift @ARGV) {
+    if (($flag eq '-v') or ($flag eq '--verbose')) {
+       $Verbose = 1;
+    }
+    if (($flag eq '-t') or ($flag eq '--ticketid')) {
+       $ReturnTid = 1;
+    }
+    
+    if (($flag eq '-d') or ($flag eq '--debug')) {
+       $RT::Logger->debug("Debug mode enabled\n");
+       $Debug = 1;
+      }
+    
+    if (($flag eq '-q') or ($flag eq '--queue')) {
+       $Queue = shift @ARGV;
+    } 
+    if ($flag eq '--ticket-id-from-extension') {
+       $TicketId = $ENV{'EXTENSION'};
+    }
+    if ($flag eq '--queue-from-extension') {
+       $Queue = $ENV{'EXTENSION'};
+    }
+    if ($flag eq '--owner-from-extension') {
+        $AssignTicketTo = $ENV{'EXTENSION'};
+    }
+
+    if (($flag eq '-a') or ($flag eq '--action')) {
+         $Action = shift @ARGV;
+    } 
+    
+    
+}
+
+# }}}
+
+# get the current mime entity from stdin
+my ($entity, $head) = ParseMIMEEntityFromSTDIN();
+
+#Get someone to send runtime errors to;
+my $ErrorsTo = ParseErrorsToAddressFromHead($head);
+
+#Get us a current user object.
+my $CurrentUser = GetCurrentUser($head, $entity, $ErrorsTo);
+
+# We've already performed a warning and sent the mail off to somewhere safe ($RTOwner).
+#  this is _exceedingly_ unlikely but we don't want to keep going if we don't have a current user
+
+unless ($CurrentUser->Id) {
+       exit(1);
+}
+
+my $MessageId = $head->get('Message-Id') || 
+  "<no-message-id-".time.rand(2000)."\@.$RT::Organization>";
+
+#Pull apart the subject line
+$Subject = $head->get('Subject') || "[no subject]";
+chomp $Subject;
+
+# Get the ticket ID unless it's already set
+$TicketId = ParseTicketId($Subject) unless ($TicketId);
+
+#Set up a queue object
+my $QueueObj = RT::Queue->new($CurrentUser);
+$QueueObj->Load($Queue);
+unless ($QueueObj->id ) {
+
+  MailError(To => $RT::OwnerEmail,
+                  Subject => "RT Bounce: $Subject",
+                  Explanation => "RT couldn't find the queue: $Queue",
+                  MIMEObj => $entity);
+
+}
+
+# {{{ Lets check for mail loops of various sorts.
+
+my $IsAutoGenerated = CheckForAutoGenerated($head);
+
+my $IsSuspiciousSender = CheckForSuspiciousSender($head);
+
+my $IsALoop = CheckForLoops($head);
+
+
+#If the message is autogenerated, we need to know, so we can not 
+# send mail to the sender
+if ($IsSuspiciousSender || $IsAutoGenerated || $IsALoop) {
+    $SquelchReplies = 1;
+
+    $ErrorsTo = $RT::OwnerEmail;
+    
+    #TODO: Is what we want to do here really 
+    #  "Make the requestor cease to get mail from RT"?
+    # This might wreak havoc with vacation-mailing users.
+    # Maybe have a "disabled for bouncing" state that gets
+    # turned off when we get a legit incoming message
+
+}
+
+
+# {{{ Warn someone  if it's a loop
+
+# Warn someone if it's a loop, before we drop it on the ground
+if ($IsALoop) {
+    $RT::Logger->crit("RT Received mail ($MessageId) from itself.");
+    
+    #Should we mail it to RTOwner?
+    if ($RT::LoopsToRTOwner) {
+       MailError(To => $RT::OwnerEmail,
+                 Subject => "RT Bounce: $Subject",
+                 Explanation => "RT thinks this message may be a bounce",
+                 MIMEObj => $entity);
+       
+       #Do we actually want to store it?
+       exit unless ($RT::StoreLoops);
+    }
+}
+
+# }}}
+
+
+   #Don't let the user stuff the RT-Squelch-Replies-To header.
+    if ($head->get('RT-Squelch-Replies-To')) {
+        $head->add('RT-Relocated-Squelch-Replies-To',
+                   $head->get('RT-Squelch-Replies-To'));
+        $head->delete('RT-Squelch-Replies-To')
+    }
+
+
+if ($SquelchReplies) {
+    ## TODO: This is a hack.  It should be some other way to
+    ## indicate that the transaction should be "silent".
+
+    my ($Sender, $junk) = ParseSenderAddressFromHead($head);
+    $head->add('RT-Squelch-Replies-To', $Sender);
+}
+
+# }}}
+
+
+# {{{ If we require that the sender be found in an external DB and they're not
+# forward this message to RTOwner
+
+
+
+if ($RT::LookupSenderInExternalDatabase && 
+    $RT::SenderMustExistInExternalDatabase )  {
+
+    MailError(To => $RT::OwnerEmail,
+             Subject => "RT Bounce: $Subject",
+             Explanation => "RT couldn't find requestor via its external database lookup",
+             MIMEObj => $entity);
+    
+}
+
+# }}}
+
+# {{{ elsif we don't have a ticket Id, we're creating a new ticket
+
+
+
+elsif (!defined($TicketId)) {
+    
+    # {{{ Create a new ticket
+    if ($Action =~ /correspond/) {
+       
+       #    open a new ticket 
+       my @Requestors = ($CurrentUser->id);
+       
+       my @Cc;
+       if ($RT::ParseNewMessageForTicketCcs) {
+               @Cc = ParseCcAddressesFromHead(Head => $head, 
+                                       CurrentUser => $CurrentUser,
+                                       QueueObj => $QueueObj );
+       }
+
+       my $Ticket = new RT::Ticket($CurrentUser);
+       my ($id, $Transaction, $ErrStr) = 
+         $Ticket->Create ( Queue => $Queue,
+                           Subject => $Subject,
+                            Owner => $AssignTicketTo,
+                           Requestor => \@Requestors,
+                           Cc => \@Cc,
+                           MIMEObj => $entity
+                         );
+       if ($id == 0 ) {
+           MailError( To => $ErrorsTo,
+                      Subject => "Ticket creation failed",
+                      Explanation => $ErrStr,
+                      MIMEObj => $entity
+                    );
+           $RT::Logger->error("Create failed: $id / $Transaction / $ErrStr ");
+       }       
+    }
+
+    # }}}
+    
+    else {
+       #TODO Return an error message
+       MailError( To => $ErrorsTo,
+                  Subject => "No ticket id specified",
+                  Explanation => "$Action aliases require a TicketId to work on",
+                  MIMEObj => $entity
+                );
+       
+       $RT::Logger->crit("$Action aliases require a TicketId to work on ".
+                         "(from ".$CurrentUser->UserObj->EmailAddress.") ".
+                         $MessageId);
+    }
+}
+
+# }}}
+
+# {{{ If we've got a ticket ID, update the ticket
+
+else {
+    
+    #   If the action is comment, add a comment.
+    if ($Action =~ /comment/i){
+       
+       my $Ticket = new RT::Ticket($CurrentUser);
+       $Ticket->Load($TicketId);
+       unless ($Ticket->Id) {
+           MailError( To => $ErrorsTo,
+                      Subject => "Comment not recorded",
+                      Explanation => "Could not find a ticket with id $TicketId",
+                      MIMEObj => $entity
+                    );
+           #Return an error message saying that Ticket "#foo" wasn't found.
+       }
+       
+       ($status, $msg) = $Ticket->Comment(MIMEObj=>$entity);
+       unless ($status) {
+           #Warn the sender that we couldn't actually submit the comment.
+           MailError( To => $ErrorsTo,
+                      Subject => "Comment not recorded",
+                      Explanation => $msg,
+                      MIMEObj => $entity
+                    );
+       }       
+    }
+
+    # If the message is correspondence, add it to the ticket
+    elsif ($Action =~ /correspond/i) {
+       my $Ticket = RT::Ticket->new($CurrentUser);
+       $Ticket->Load($TicketId);
+       
+       #TODO: Check for error conditions
+       ($status, $msg) = $Ticket->Correspond(MIMEObj => $entity);
+       unless ($status) {
+
+           #Return mail to the sender with an error
+           MailError( To => $ErrorsTo,
+                      Subject => "Correspondence not recorded",
+                      Explanation => $msg,
+                      MIMEObj => $entity
+                    );
+       }
+    }
+
+    else {
+       #Return mail to the sender with an error
+           MailError( To => $ErrorsTo,
+                      Subject => "RT Configuration error",
+                      Explanation => "'$Action' not a recognized action.".
+                                     " Your RT administrator has misconfigured ".
+                                     "the mail aliases which invoke RT" ,
+                      MIMEObj => $entity
+                    );
+
+       $RT::Logger->crit("$Action type unknown for $MessageId");
+       
+    }
+    
+}
+
+# }}}
+
+$RT::Handle->Disconnect();
+
+
+# Everything below this line is a helper sub. most of them will eventually
+# move to Interface::Email
+
+#When we call die, trap it and log->crit with the value of the die.
+$SIG{__DIE__}  = sub {
+    unless ($^S || !defined $^S ) {
+        $RT::Logger->crit("$_[0]");
+       MailError( To => $ErrorsTo,  
+                  Bcc => $RT::OwnerEmail,
+                  Subject => "RT Critical error. Message not recorded!",
+                  Explanation => "$_[0]",
+                  MIMEObj => $entity
+                );
+       exit(-1);
+    }
+    else {
+        #Get out of here if we're in an eval
+        die $_[0];
+    }
+};
+
+
+
+1;
diff --git a/rt/bin/rtadmin b/rt/bin/rtadmin
new file mode 100644 (file)
index 0000000..25ba1b0
--- /dev/null
@@ -0,0 +1,1040 @@
+#!!!PERL!! -w
+#
+# $Header: /home/cvs/cvsroot/freeside/rt/bin/Attic/rtadmin,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# RT is (c) 1996-2001 Jesse Vincent <jesse@fsck.com>
+
+use strict;
+use Carp;
+use Getopt::Long qw(:config pass_through);
+
+use lib "!!RT_LIB_PATH!!";
+use lib "!!RT_ETC_PATH!!";
+
+use RT::Interface::CLI  qw(CleanEnv LoadConfig DBConnect 
+                          GetCurrentUser GetMessageContent);
+
+#Clean out all the nasties from the environment
+CleanEnv();
+
+#Load etc/config.pm and drop privs
+LoadConfig();
+
+#Connect to the database and get RT::SystemUser and RT::Nobody loaded
+DBConnect();
+
+#Drop setgid permissions
+RT::DropSetGIDPermissions();
+
+#Get the current user all loaded
+my $CurrentUser = GetCurrentUser();
+
+unless ($CurrentUser->Id) {
+        print "No RT user found. Please consult your RT administrator.\n";   
+        exit(1);
+}
+
+
+
+
+PickMode();
+
+
+# {{{ Help
+
+sub Help {
+
+    # {{{ help_acl
+my $help_acl ="
+        Access control
+       --grant-right <right> 
+       --revoke-right <right>
+         --userid <user>
+          --groupid <group>
+       --list-rights";
+
+# }}}
+    
+    # {{{ help_keyword_sel
+my $help_keyword_sel = "
+       Keyword Selections
+       --add-keyword-select
+       --modify-keyword-select <name>
+          --ks-name <name>
+          --ks-keyword <keyword>
+          --ks-single 
+          --ks-multiple
+          --ks-depth  <int>
+
+        --disable-keyword-select <name>";
+# }}}
+    
+    # {{{ help_scrip
+my $help_scrip = "
+        Scrips
+        --create-scrip
+           --scrip-condition <condition name or id>
+           --scrip-action <action name or id>
+           --scrip-template <template name or id>
+
+        --delete-scrip <id>
+       --list-scrips";
+
+# }}}
+    
+    # {{{ help_template  
+my $help_template = "      
+        Templates
+        --delete-template [<id>|<name>]
+        --display-template [<id>|<name>]
+
+         --create-template
+         --modify-template [<id>|<name>]
+           Flags for --create-template and --modify-template
+           --template-name
+           --template-description
+           --template-edit-content
+         
+         --list-templates";
+
+# }}}
+    
+    
+print <<EOF;
+
+USAGE:  rtadmin --user <userid> [Userflags]
+       rtadmin --list-users
+       rtadmin --queue <queueid> [Queueflags]
+       rtadmin --list-queues
+       rtadmin --group [groupflags]
+       rtadmin --list-groups
+       rtadmin --system  [SystemFlags]
+       rtadmin --keyword [keywordflags]
+
+User configuration for --user <userid>
+   
+       --disable
+       --create
+       --display
+
+  Core Attributes
+       --userid
+       --gecos
+       --password
+       --emailaddress
+       --privileged
+       --comments
+       --signature
+       --organization
+
+  Names        
+       --realname
+       --nickname
+       
+  Auth and external info
+       --externalcontactinfoid
+       --contactinfosystem
+       --externalauthid
+       --authsystem
+
+  Phone numbers
+       --pagerphone
+       --workphone
+       --mobilemphone
+       --homephone
+
+  Paper address
+       --address1
+       --address2
+       --city
+       --state
+       --zip
+       --country
+       --freeformcontactqinfo  
+
+
+Group Configuration for --group <groupid>
+       --create 
+       --delete
+       --display
+
+       --name <new name>
+       --description <new description>
+
+
+
+       --add-member <userid>
+       --delete-member <userid>
+       --list-members  
+
+Queue Configuration for --queue <queueid>
+       --create
+       --disable
+       --display
+
+       --name <name>
+       --correspondaddress <email address>
+       --commentaddress <email address>
+       --initialpriority <int>
+       --finalpriority <int>
+       --defaultduein <days>
+
+       --add-cc <email address>
+       --delete-cc <email address>
+       --add-admincc <email address>
+       --delete-admincc <email address>
+        --list-watchers
+
+       
+
+$help_acl
+
+$help_keyword_sel
+
+$help_template
+
+$help_scrip
+
+
+System configuration for --system 
+
+$help_acl
+
+$help_keyword_sel
+
+$help_template
+
+$help_scrip
+
+
+Keyword configuration for --keyword  <fully qualified name>
+       --list-children
+       --create-child <name>
+       --disable
+       --name <new name>
+       --description <new description>
+
+EOF
+
+
+
+}
+
+# }}}
+
+# {{{ PickMode
+
+sub PickMode {
+    my ($user,$group, $queue, $system, $keyword, $listusers, 
+       $listgroups, $listqueues, $help);
+
+
+    GetOptions ('help|h|usage' => \$help,
+               'user=s' => \$user,
+               'queue=s' => \$queue,
+               'group=s' => \$group,
+               'system' => \$system,
+               'keyword=s', => \$keyword,
+               'list-users' => \$listusers,
+               'list-queues' => \$listqueues,
+               'list-groups' => \$listgroups,
+              );
+    
+
+    
+    if ($user)          { AdminUser($user) }  
+    elsif ($group)      { AdminGroup($group) }
+    elsif ($queue)      { AdminQueue($queue) }
+    elsif ($system)     { AdminSystem($system) }
+    elsif ($keyword)    { AdminKeywords($keyword) }
+    elsif ($listusers)  { ListUsers() }
+    elsif ($listgroups) { ListGroups()  }
+    elsif ($listqueues) { ListQueues() }
+    elsif ($help)       { Help()}
+    else {
+       print "No command found\n";
+    }
+    exit(0);
+}      
+
+# }}}
+
+# {{{ AdminUser
+
+sub AdminUser {
+    my $user=shift;
+    my %args;
+
+    GetOptions(\%args,
+          'create', 'disable|delete', 'display',
+          'Name=s', 'Gecos=s', 'Password=s',
+          'EmailAddress=s', 'Privileged=s', 'Comments=s', 'Signature=s',
+          'Organization=s', 'RealName=s', 'NickName=s',  
+          'ExternalContactInfoId=s', 'ContactInfoSystem=s', 
+           'ExternalAuthId=s', 'AuthSystem=s',
+          'HomePhone=s', 'WorkPhone=s', 'MobilePhone=s', 'PagerPhone=s',
+           'Address1=s', 'Address2=s', 'City=s', 'State=s', 'Zip=s', 
+           'Country=s', 'FreeformContactInfo=s');
+
+    my $user_obj = new RT::User($CurrentUser);
+    
+    #Create the user if we need to 
+    if ($args{'create'}) {
+       my ($status, $msg) = 
+         $user_obj->Create( Name => ($args{'Name'} || $user),
+                            Gecos => $args{'Gecos'},
+                            Password => $args{'Password'},
+                            EmailAddress => $args{'EmailAddress'},
+                            Privileged => $args{'Privileged'},
+                            Comments => $args{'Comments'},
+                            Signature => $args{'Signature'},
+                            Organization => $args{'Organization'},
+                            RealName => $args{'RealName'},
+                            NickName => $args{'NickName'},
+                            ExternalContactInfoId => $args{'ExternalContactInfoId'},
+                            ContactInfoSystem => $args{'ContactInfoSystem'},
+                            ExternalAuthId => $args{'ExternalAuthId'},
+                            AuthSystem => $args{'AuthSystem'},
+                            HomePhone => $args{'HomePhone'},
+                            WorkPhone => $args{'WorkPhone'},
+                            MobilePhone => $args{'MobilePhone'},
+                            PagerPhone => $args{'PagerPhone'},
+                            Address1 => $args{'Address1'},
+                            Address2 => $args{'Address2'},
+                            City  => $args{'City'},
+                            State => $args{'State'},
+                            Zip => $args{'Zip'},
+                            FreeformContactInfo => $args{'FreeformContactInfo'}
+                          );
+       
+       print "$msg\n";
+       return();
+       
+    }
+    else {
+       
+       
+       #Load the user
+       $user_obj->Load($user);
+       
+       unless ($user_obj->id) {
+           print "User '$user' not found\n";
+           return();
+       }       
+       
+       
+       
+       #modify the user if we need to
+       my @attributes = ('Name', 'Gecos',
+                         'EmailAddress', 'Privileged', 'Comments', 'Signature',
+                         'Organization', 'RealName', 'NickName',  
+                         'ExternalContactInfoId', 'ContactInfoSystem', 
+                         'ExternalAuthId', 'AuthSystem',
+                         'HomePhone', 'WorkPhone', 'MobilePhone', 'PagerPhone',
+                         'Address1', 'Address2', 'City', 'State', 'Zip', 
+                         'Country', 'FreeformContactInfo');
+       foreach my $attrib (@attributes) {
+           if ( (exists ($args{"$attrib"})) and
+                ($user_obj->$attrib() ne $args{"$attrib"})) {
+               
+               my $method = "Set$attrib";
+               my ($val, $msg) = $user_obj->$method($args{"$attrib"});
+               print "User ".$user_obj->Name. " $attrib:  $msg\n";
+               
+           }
+       }               
+       
+       if (exists ($args{'Password'})) {
+           my ($code, $msg);
+           ($code, $msg) = $user_obj->SetPassword($args{'Password'});
+           print "User ". $user_obj->Name. ' Password: '. $msg . "\n";
+       }
+       
+       #Check if we need to display the user
+       if ($args{'display'}) {
+           foreach my $attrib (@attributes) {
+               next if ($attrib eq 'Password'); #Can't see the password
+               printf("%22.22s %-64s\n",$attrib, ($user_obj->$attrib()||'(undefined)'));
+               
+           }   
+       }       
+       
+       #Check if we need to delete the user
+       if ($args{'disable'}) {
+           my ($val, $msg) = $user_obj->SetDisabled(1);
+           print "$msg\n";
+       }       
+       
+    }
+}
+
+# }}}
+
+# {{{ AdminQueue
+
+sub AdminQueue {
+    my $queue=shift;
+    my %args;
+
+    GetOptions(\%args,
+              'create', 'disable|delete', 'display',
+              'Name=s', 'CorrespondAddress=s', 'Description=s',
+              'CommentAddress=s', 'InitialPriority=n', 'FinalPriority=n',
+              'DefaultDueIn=n',
+              
+              'add-cc=s@', 'add-admincc=s@', 
+              'delete-cc=s@', 'delete-admincc=s@',
+              'list-watchers', 'create-template'
+              );
+
+    use RT::Queue;
+    my $queue_obj = new RT::Queue($CurrentUser);
+    
+    #Create the queue if we need to 
+    if ($args{'create'}) {
+       my ($status, $msg) = 
+         $queue_obj->Create(
+                            Name => ($args{'Name'} || $queue) ,
+                            CorrespondAddress => $args{'CorrespondAddress'},
+                            Description => $args{'Description'},
+                            CommentAddress  => $args{'CommentAddress'},
+                            InitialPriority => $args{'InitialPriority'},
+                            FinalPriority => $args{'FinalPriority'},
+                            DefaultDueIn => $args{'DefaultDueIn'}
+                           );
+       
+       print "$msg\n";
+    }
+    else {
+       #Load the queue
+       $queue_obj->Load($queue);
+       
+       unless ($queue_obj->id) {
+           print "Queue '$queue' not found\n";
+           return();
+       }       
+               
+        #modify if we need to
+       my @attributes = qw(Name CorrespondAddress Description
+                           CommentAddress InitialPriority FinalPriority
+                           DefaultDueIn
+                        );
+       foreach my $attrib (@attributes) {
+           if ( (exists ($args{"$attrib"})) and
+                ($queue_obj->$attrib() ne $args{"$attrib"})) {
+               
+               my $method = "Set$attrib";
+               my ($val, $msg) = $queue_obj->$method($args{"$attrib"});
+               print "Queue ".$queue_obj->Name. " $attrib:  $msg\n";
+               
+           }
+       }               
+           
+       
+       #Check if we need to display the queue
+       if ($args{'display'}) {
+           foreach my $attrib (@attributes) {
+               printf("%22.22s %-64s\n",$attrib, ($queue_obj->$attrib()||'(undefined)'));
+               
+           }   
+       }       
+               
+       foreach my $person (@{$args{'add-cc'}}) {
+           my ($val, $msg) = $queue_obj->AddCc(Email => $person);
+           print "$msg\n";
+       }
+       foreach my $person (@{$args{'add-admincc'}}) {
+           my ($val, $msg) = $queue_obj->AddAdminCc(Email => $person);
+           print "$msg\n";
+       }
+
+       foreach my $person (@{$args{'delete-cc'}}) {
+           my ($val, $msg) = $queue_obj->DeleteCc($person);
+           print "$msg\n";
+       }
+       foreach my $person (@{$args{'delete-admincc'}}) {
+           my ($val, $msg) = $queue_obj->DeleteAdminCc($person);
+           print "$msg\n";
+       }
+
+       if ($args{'list-watchers'}) {
+           require RT::Watchers;
+           my $watchers = new RT::Watchers($CurrentUser);
+           $watchers->LimitToQueue($queue_obj->id);
+           while (my $watcher = $watchers->Next()) {
+               printf("%10s %-60s\n",
+                      $watcher->Type, $watcher->Email );
+           }   
+       }       
+
+        AdminTemplates($queue_obj->Id());
+        AdminScrips($queue_obj->Id());
+       AdminRights($queue_obj->Id());
+       AdminKeywordSelects($queue_obj->Id());
+
+       #Check if we need to delete the queue
+       if ($args{'disable'}) {
+           my ($val, $msg) = $queue_obj->SetDisabled(1);
+           print "$msg\n";
+       }       
+       
+    }
+}
+
+# }}}
+
+# {{{ AdminKeywords
+
+sub AdminKeywords {
+    my $keyword = shift;
+    
+    my %args;
+    GetOptions(\%args, 'list-children', 'create-child=s', 'disable|delete', 'Name=s', 'Description=s');
+    
+    use RT::Keyword;
+    
+    my $key_obj = new RT::Keyword($CurrentUser);
+    my $key_id;
+    
+    #If we're dealing with the root of the keyword list
+    if ($keyword eq '/') {
+       $key_id=0;
+    }  
+    else {
+       my ($val, $msg) = $key_obj->LoadByPath( $keyword );
+       unless ($val) {
+           print $msg ."\n";
+       }
+       $key_id = $key_obj->Id();
+    }  
+    
+    if ($args{'create-child'}) {
+       my $child = new RT::Keyword($CurrentUser);
+       
+       my ($val, $msg) = $child->Create( Parent => $key_id,
+                                         Name => $args{'create-child'},
+                                       );
+       print $msg ."\n";
+    }  
+    
+    elsif ($args{'list-children'}) {
+       my $keywords;
+       if ($key_obj->id) {
+           $keywords = $key_obj->Children();
+       }       
+       #If we didn't actually have a keyword object, we need to create our own Keywords object.
+       else {
+           $keywords = new RT::Keywords($CurrentUser);
+           $keywords->LimitToParent(0); 
+       }
+       
+       while (my $key=$keywords->Next) {
+           print $key->Name;
+           if ($key->Description) {
+               print " (" . $key->Description .")";
+           }   
+           print "\n";
+       }       
+             
+
+    }  
+    
+    #Else we wanna do some modification.
+    else {
+       
+       #If we didn't load a keyword, get out
+       return(undef) unless ($key_obj->Id);
+       
+       
+       my @attributes = qw( Name Description );
+       foreach my $attrib (@attributes) {
+           if ( (exists ($args{"$attrib"})) and
+                ($key_obj->$attrib() ne $args{"$attrib"})) {
+               
+               my $method = "Set$attrib";
+               my ($val, $msg) = $key_obj->$method($args{"$attrib"});
+               
+               print "Keyword ".$key_obj->Name. " $attrib:  $msg\n";       }
+       }
+       
+       if ($args{'disable'}) {
+           $key_obj->SetDisabled(1);
+           
+       }
+       
+    }
+}
+
+# }}}
+
+# {{{ AdminKeywordSelects
+
+sub AdminKeywordSelects {
+    my $queue = shift;
+    # O for queue means global
+
+    my %args;
+    GetOptions(\%args, 'add-keyword-select','disable-keyword-select|delete-keyword-select=s',
+              'modify-keyword-select=s', 
+              'keyword-select-Keyword|ks-keyword=s', 
+              'keyword-select-Single|ks-single', 
+              'keyword-select-Multiple|ks-multiple', 
+              'keyword-select-Depth|ks-depth=i', 
+              'keyword-select-Name|ks-name=s'
+             );
+
+    # sanitize single vs multiple.
+    if ($args{'keyword-select-Multiple'}) {
+       $args{'keyword-select-Single'} = 0;
+    }
+    
+    use RT::KeywordSelect;
+    my $keysel_obj = new RT::KeywordSelect($CurrentUser);
+    if ($args{'add-keyword-select'}) {
+       
+       my ($val, $msg) = $keysel_obj->Create( Keyword => $args{'keyword-select-Keyword'},
+                                              Depth => $args{'keyword-select-Depth'},
+                                              Single => $args{'keyword-select-Single'},
+                                              Name => $args{'keyword-select-Name'},
+                                              ObjectType => 'Ticket',
+                                              ObjectField => 'Queue',
+                                              ObjectValue => $queue);
+       print $msg ."\n";
+    }  
+    elsif ($args{'modify-keyword-select'}) {
+       $keysel_obj->LoadByName(Name => $args{'modify-keyword-select'},
+                               Queue => $queue
+                              );
+       
+       unless ($keysel_obj->Id()) {
+           print "Keyword select not found\n";
+           return();
+       }       
+       my @attributes = qw( Name Keyword Single Depth );
+       foreach my $attrib (@attributes) {
+           if ( (exists ($args{"keyword-select-$attrib"})) and
+                ($keysel_obj->$attrib() ne $args{"keyword-select-$attrib"})) {
+               
+               my $method = "Set$attrib";
+               my ($val, $msg) = $keysel_obj->$method($args{"keyword-select-$attrib"});
+                               
+               print "Keyword select ".$keysel_obj->Name. " $attrib:  $msg\n";     }
+       }
+
+
+    }  
+
+    
+    elsif ($args{'disable-keyword-select'}) {
+       $keysel_obj->LoadByName(Name => $args{'disable-keyword-select'},
+                               Queue => $queue);
+       
+       $keysel_obj->SetDisabled(1);
+       
+    }
+}
+
+# }}}
+
+# {{{ AdminGroup
+
+sub AdminGroup {
+    my $group = shift;
+
+    my (%args);
+
+    GetOptions(\%args,
+              'create', 'delete', 'display',
+              'Name=s', 'Description=s',
+                      
+              'add-member=s@', 'delete-member=s@',
+              'list-members'
+              );
+
+
+    use RT::Group;
+    my $group_obj = new RT::Group($CurrentUser);
+    unless ($group) {
+       print "Group not specified.\n";
+       return();
+    }  
+    
+
+    #Create the group if we need to
+    if ($args{'create'}) {
+       my ($val, $msg) = $group_obj->Create( Name => ($args{'Name'} || $group),
+                                             Description => $args{'Description'} );
+       print $msg ."\n";
+    }  
+    #otherwise we load it
+    else {
+       $group_obj->Load($group);
+    }  
+    
+    #If we have no group object, get the hell out
+    unless ($group_obj->Id) {
+       print "Group not found.\n";
+    }
+
+    if ($args{'delete'}) {
+       my ($val, $msg) = $group_obj->Delete();
+       print $msg ."\n";
+       return();
+    }  
+
+
+    
+    #modify if we need to
+    my @attributes = qw(Name Description
+                       
+                      );
+    foreach my $attrib (@attributes) {
+       if ( (exists ($args{"$attrib"})) and
+            ($group_obj->$attrib() ne $args{"$attrib"})) {
+               
+           my $method = "Set$attrib";
+           my ($val, $msg) = $group_obj->$method($args{"$attrib"});
+           print "Group ".$group_obj->Name. " $attrib:  $msg\n";
+           
+           }
+    }          
+    
+    foreach my $user (@{$args{'add-member'}}) {
+       my ($val, $msg) = $group_obj->AddMember($user);
+       print $msg. "\n";
+    }
+    foreach my $user (@{$args{'delete-member'}}) {
+       my ($val, $msg) = $group_obj->DeleteMember($user);
+       print $msg ."\n";
+    }
+    
+    if ($args{'list-members'}) {
+       my $members = $group_obj->MembersObj();
+       while (my $member = $members->Next()) {
+           print $member->UserObj->Name() ."\n";
+       }
+    }  
+    
+}
+
+# }}}
+
+# {{{ AdminSystem
+sub AdminSystem {
+    print "In AdminSystem\n";
+
+    AdminTemplates(0);
+    AdminScrips(0);
+    AdminRights(0);
+    AdminKeywordSelects(0);
+}
+# }}}
+
+# {{{ sub AdminTemplates
+
+sub AdminTemplates {
+    my $queue = shift;
+    #Queue = 0 means 'global';
+
+    my %args;
+
+    
+    GetOptions(\%args, 'list-templates', 'create-template','modify-template=s',
+              'delete-template=s', 'display-template=s',
+              'template-Name=s', 'template-Description=s',
+              'template-edit-content!');
+    
+    # {{{ List templates
+    if ($args{'list-templates'}) {
+       print "Templates for $queue\n";
+       require RT::Templates;
+       my $templates = new RT::Templates($CurrentUser);
+       if ($queue != 0) {
+           $templates->LimitToQueue($queue);
+       }       
+       else {
+           $templates->LimitToGlobal();
+       }       
+       while (my $template = $templates->Next) {
+           print $template->Id.": ".$template->Name." - " . $template->Description ."\n";
+       }       
+    }
+
+    # }}}
+
+    require RT::Template;      
+    my $template = new RT::Template($CurrentUser);
+    if ($args{'delete-template'}) {
+       $template->Load($args{'delete-template'});
+       unless ($template->id) {
+           print "Couldn't load template";
+           return(undef);
+       }
+       my ($val, $msg) = $template->Delete();
+       print "$msg\n";
+    }
+    elsif ($args{'create-template'}) {
+       #TODO edit the template content
+       my $content;
+
+       my $linesref = GetMessageContent(CurrentUser => $CurrentUser,
+                                     Edit => 1);
+       
+       $content = join("\n", @{$linesref});
+       
+
+       my ($val, $msg) = $template->Create(Name => $args{'template-Name'},
+                                           Description => $args{'template-Description'},
+                                           Content => $content,
+                                           Queue => $queue);
+       print "$msg\n";
+    }  
+    elsif ($args{'modify-template'}) {
+       
+       $template->Load($args{'modify-template'});
+       unless ($template->Id()) {
+           print "Template not found\n";
+           return();
+       }       
+       my @attributes = qw( Name Description );
+       foreach my $attrib (@attributes) {
+           if ( (exists ($args{"template-$attrib"})) and
+                ($template->$attrib() ne $args{"template-$attrib"})) {
+               
+               my $method = "Set$attrib";
+               my $val = $template->$method($args{"template-$attrib"});
+                               
+           }
+       }
+       if ($args{'template-edit-content'}) {
+           
+           my $linesref = GetMessageContent(CurrentUser => $CurrentUser,
+                                            Content => $template->Content,
+                                            Edit => 1);
+           
+           my $content = join("\n", @{$linesref});         
+           my ($val) = $template->SetContent($content);
+           print $val."\n";
+       }       
+
+    }  
+    if ($args{'display-template'}) {
+       $template->Load($args{'display-template'});
+       print $template->Name . "\n". $template->Description ."\n". $template->Content."\n";
+    }  
+}      
+
+# }}}
+
+# {{{ sub AdminScrips
+
+sub AdminScrips {
+    my $queue = shift;
+    #Queue = 0 means 'global';
+
+    my %args;
+
+    
+    GetOptions(\%args, 'list-scrips', 'create-scrip','modify-scrip=s',
+              'scrip-action=s', 'scrip-template=s', 'scrip-condition=s',
+              'delete-scrip=s');
+
+    
+    # {{{ List entries
+    if ($args{'list-scrips'}) {
+       print "Scrips for $queue\n";
+       require RT::Scrips;
+       my $scrips = new RT::Scrips($CurrentUser);
+       if ($queue != 0) {
+           $scrips->LimitToQueue($queue);
+       }       
+       else {
+           $scrips->LimitToGlobal();
+       }       
+       while (my $scrip = $scrips->Next) {
+           print $scrip->Id.": If ".
+             $scrip->ConditionObj->Name." then " .
+               $scrip->ActionObj->Name." with template " .
+                 $scrip->TemplateObj->Name."\n";
+       }       
+    }
+
+    # }}}
+
+    require RT::Scrip;
+    my $scrip = new RT::Scrip($CurrentUser);
+    if ($args{'delete-scrip'}) {
+       $scrip->Load($args{'delete-scrip'});
+       unless ($scrip->id) {
+           print "Couldn't load scrip";
+             return(undef);
+       }
+       my ($val, $msg) = $scrip->Delete();
+       print "$msg\n";
+    }
+    elsif ($args{'create-scrip'}) {
+       my ($val, $msg) = $scrip->Create( ScripAction => $args{'scrip-action'},
+                                         ScripCondition => $args{'scrip-condition'},
+                                         Template => $args{'scrip-template'},
+                                         Queue => $queue);
+
+       print "$msg\n";
+    }  
+}      
+
+# }}}
+
+# {{{ sub AdminRights
+
+sub AdminRights {
+    my $queue = shift;
+    #Queue = 0 means 'global';
+
+    my ($scope, $appliesto);
+    if ($queue == 0) {
+       $scope = 'System';
+       $appliesto = 0;
+    }  
+    else {
+       $scope =  'Queue';
+       $appliesto = $queue;
+    }  
+
+    my %args;    
+    GetOptions(\%args, 
+              'grant-right|add-right|new-right|create-right=s@',
+              'revoke-right|del-right|delete-right=s@',
+              'list-rights', 'userid=s@', 'groupid=s@',
+             );
+    
+    
+    # {{{ List entries
+    if ($args{'list-rights'}) {
+       require RT::ACL;
+       my $acl = new RT::ACL($CurrentUser);
+       if ($queue != 0) {
+           $acl->LimitToQueue($queue);
+       }       
+       else {
+           $acl->LimitToSystem();
+       }       
+       while (my $ace = $acl->Next) {
+           print $ace->RightScope;
+           
+           #Print the queue name if we have it.
+           print " " . $ace->AppliesToObj->Name if (defined $ace->AppliesToObj);
+           
+           print ": ". $ace->PrincipalType . " " .$ace->PrincipalObj->Name .
+             " has right " . $ace->RightName ."\n";
+             
+       }       
+    }
+
+    # }}}
+
+    require RT::ACE;    
+
+    # {{{ Build up an array of principals
+    my (@principals);
+    my $i = 0;
+    foreach my $group (@{$args{'groupid'}}) { 
+
+
+       my $princ = new RT::Group($CurrentUser);
+       $princ->Load("$group");
+       if ($princ->id) {
+           $principals[$i]->{'type'} = 'Group';
+           $principals[$i]->{'id'} = $princ->id();
+           $i++;
+       }       
+       else {
+           print "Could not find group $group\n";
+       }       
+    }  
+
+
+    foreach my $user (@{$args{'userid'}}) { 
+       my $princ = new RT::User($CurrentUser);
+       $princ->Load("$user");
+       if ($princ->id) {
+           $principals[$i]->{'type'} = 'User';
+           $principals[$i]->{'id'} = $princ->id();
+           $i++;
+       }       
+       else {
+           print "Could not find user $user.\n";
+           }
+    }
+    # }}}
+
+
+    foreach my $principal (@principals) {
+
+       # {{{ Delete rights that need deleting
+       foreach my $right (@{$args{'revoke-right'}}) {
+           my $ace = new RT::ACE($CurrentUser);
+           $RT::Logger->debug("Trying to delete a right: $right \n");
+           my ($val, $msg) = $ace->LoadByValues( RightName => $right,
+                                                 RightScope => $scope,
+                                                 PrincipalType => $principal->{'type'},
+                                                 PrincipalId => $principal->{'id'},
+                                                 RightAppliesTo => $appliesto);
+           
+           unless ($val) {
+               print "Right $right not found for" . $principal->{'type'} . " " .
+                 $principal->{'id'} . " in scope $scope ($appliesto)\n";
+               next;
+           }   
+           my ($delval, $delmsg) =$ace->Delete;
+           print "$delmsg\n";
+           
+           
+       }
+
+       # }}}
+       
+       # {{{ grant rights that need granting
+       foreach my $right (@{$args{'grant-right'}}) {
+           my $ace = new RT::ACE($CurrentUser);
+           my ($val, $msg) = $ace->Create(RightName => $right,
+                                          PrincipalType => $principal->{'type'},
+                                          PrincipalId => $principal->{'id'},
+                                          RightScope => $scope,
+                                          RightAppliesTo => $appliesto);
+           
+           print $msg . "\n";
+       }
+
+       # }}}
+    }  
+
+}
+
+# }}}
+
+
+sub ListUsers {
+    require RT::Users;
+    my $users = new RT::Users($CurrentUser);
+    $users->UnLimit();
+    while (my $user = $users->Next()) {
+       printf ("%16s %-16s\n",$user->Name(), $user->EmailAddress());
+    }
+}
+sub ListQueues {
+    require RT::Queues;
+    my $queues = new RT::Queues($CurrentUser);
+    $queues->UnLimit();
+    while (my $queue = $queues->Next()) {
+       printf ("%16s %-16s\n",$queue->Name(), $queue->Description());
+    }
+}
+
+sub ListGroups {
+    require RT::Groups;
+    my $groups = new RT::Groups($CurrentUser);
+    $groups->UnLimit();
+    while (my $group = $groups->Next()) {
+       printf ("%16s %-16s\n",$group->Name(), $group->Description());
+    }  
+}
diff --git a/rt/bin/webmux.pl b/rt/bin/webmux.pl
new file mode 100755 (executable)
index 0000000..6e1ae06
--- /dev/null
@@ -0,0 +1,177 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/bin/Attic/webmux.pl,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# RT is (c) 1996-2000 Jesse Vincent (jesse@fsck.com);
+
+use strict;
+$ENV{'PATH'} = '/bin:/usr/bin';    # or whatever you need
+$ENV{'CDPATH'} = '' if defined $ENV{'CDPATH'};
+$ENV{'SHELL'} = '/bin/sh' if defined $ENV{'SHELL'};
+$ENV{'ENV'} = '' if defined $ENV{'ENV'};
+$ENV{'IFS'} = ''          if defined $ENV{'IFS'};
+
+
+# We really don't want apache to try to eat all vm
+# see http://perl.apache.org/guide/control.html#Preventing_mod_perl_Processes_Fr
+
+
+package RT::Mason;
+
+use CGI qw(-private_tempfiles); #bring this in before mason, to make sure we
+                               #set private_tempfiles
+use HTML::Mason::ApacheHandler (args_method => 'CGI');
+use HTML::Mason;  # brings in subpackages: Parser, Interp, etc.
+
+use vars qw($VERSION %session $Nobody $SystemUser $r $m);
+
+# List of modules that you want to use from components (see Admin
+# manual for details)
+
+#Clean up our umask...so that the session files aren't world readable, writable or executable
+umask(0077);
+
+
+         
+$VERSION="!!RT_VERSION!!";
+
+use lib "!!RT_LIB_PATH!!";
+use lib "!!RT_ETC_PATH!!";
+
+#This drags in  RT's config.pm
+use config;
+use Carp;
+
+{  
+           package HTML::Mason::Commands;
+           use vars qw(%session $m);
+         
+           use RT; 
+           use RT::Ticket;
+           use RT::Tickets;
+           use RT::Transaction;
+           use RT::Transactions;
+           use RT::User;
+           use RT::Users;
+           use RT::CurrentUser;
+           use RT::Template;
+           use RT::Templates;
+           use RT::Queue;
+           use RT::Queues;
+           use RT::ScripAction;
+           use RT::ScripActions;
+           use RT::ScripCondition;
+           use RT::ScripConditions;
+           use RT::Scrip;
+           use RT::Scrips;
+           use RT::Group;
+           use RT::Groups;
+           use RT::Keyword;
+           use RT::Keywords;
+           use RT::ObjectKeyword;
+           use RT::ObjectKeywords;
+           use RT::KeywordSelect;
+           use RT::KeywordSelects;
+           use RT::GroupMember;
+           use RT::GroupMembers;
+           use RT::Watcher;
+           use RT::Watchers;
+           use RT::Handle;
+           use RT::Interface::Web;    
+           use MIME::Entity;
+           use Text::Wrapper;
+           use Apache::Cookie;
+           use Date::Parse;
+           use HTML::Entities;
+           
+           #TODO: make this use DBI
+           use Apache::Session::File;
+
+           # Set this page's content type to whatever we are called with
+           sub SetContentType {
+               my $type = shift;
+               $RT::Mason::r->content_type($type);
+           }
+
+           sub CGIObject {
+               $m->cgi_object();
+           }
+
+       }
+my ($parser, $interp, $ah);
+if ($HTML::Mason::VERSION < 1.0902) {
+ $parser = &RT::Interface::Web::NewParser(allow_globals => [%session]);
+
+ $interp = &RT::Interface::Web::NewInterp(parser=>$parser,
+                                          allow_recursive_autohandlers => 1,
+                                                              );
+
+ $ah = &RT::Interface::Web::NewApacheHandler($interp);
+} else {
+ $ah = &RT::Interface::Web::NewMason11ApacheHandler();
+}
+# Activate the following if running httpd as root (the normal case).
+# Resets ownership of all files created by Mason at startup.
+#
+chown (Apache->server->uid, Apache->server->gid, 
+               [$RT::MasonSessionDir]);
+
+
+chown (Apache->server->uid, Apache->server->gid, 
+               $ah->interp->files_written);
+
+# Die if WebSessionDir doesn't exist or we can't write to it
+
+stat ($RT::MasonSessionDir);
+die "Can't read and write $RT::MasonSessionDir"
+  unless (( -d _ ) and ( -r _ ) and ( -w _ ));
+
+
+sub handler {
+    ($r) = @_;
+    
+    RT::Init();
+    # We don't need to handle non-text items
+    return -1 if defined($r->content_type) && $r->content_type !~ m|^text/|io;
+    
+    #This is all largely cut and pasted from mason's session_handler.pl
+    
+    my %cookies = Apache::Cookie::parse($r->header_in('Cookie'));
+    
+    eval { 
+       tie %HTML::Mason::Commands::session, 'Apache::Session::File',
+         ( $cookies{'AF_SID'} ? $cookies{'AF_SID'}->value() : undef ), 
+           { Directory => $RT::MasonSessionDir,
+             LockDirectory => $RT::MasonSessionDir,
+           }   ;
+    };
+    
+    if ( $@ ) {
+       # If the session is invalid, create a new session.
+       if ( $@ =~ m#^Object does not exist in the data store# ) {
+            tie %HTML::Mason::Commands::session, 'Apache::Session::File', undef,
+            { Directory => $RT::MasonSessionDir,
+              LockDirectory => $RT::MasonSessionDir,
+            };
+            undef $cookies{'AF_SID'};
+       }
+         else {
+            die "RT Couldn't write to session directory '$RT::MasonSessionDir'. Check that this directory's permissions are correct.";
+         }
+    }
+    
+    if ( !$cookies{'AF_SID'} ) {
+       my $cookie = new Apache::Cookie
+         ($r,
+          -name=>'AF_SID', 
+          -value=>$HTML::Mason::Commands::session{_session_id}, 
+          -path => '/',);
+       $cookie->bake;
+
+    }
+    my $status = $ah->handle_request($r);
+    untie %HTML::Mason::Commands::session;
+    
+    return $status;
+    
+  }
+1;
+
diff --git a/rt/docs/README.docs b/rt/docs/README.docs
new file mode 100755 (executable)
index 0000000..38866b3
--- /dev/null
@@ -0,0 +1,2 @@
+Questions about docs should be sent to the RT Documentation Team (rt-docs@fsck.com)
+which is led by Meri.
diff --git a/rt/docs/Security b/rt/docs/Security
new file mode 100644 (file)
index 0000000..c9787ac
--- /dev/null
@@ -0,0 +1,25 @@
+RT2 runs setgid to some group (it defaults to 'rt').
+
+rt's configuration file, 'config.pm', is not world readable because it 
+contains rt's database password. If  a user gets access to this file, he
+can arbitrarily manipulate the RT database. This is bad. You don't want
+this to happen.  config.pm is mode 550. No users should be members of 
+the 'rt' group unless you want them to be able to obtain your rt password.
+
+If you're running the web interface, you'll need to make sure your webserver
+has access to config.pm.  You could do this by letting your webserver's user
+be a member of the 'rt' group. This has the disadvantage of letting 
+any mod_perl code on your web server have access to your RT password.
+
+Alternatively, you can run RT2 on its own apache instance bound to a high
+port on 127.0.0.1
+which runs as a non-priviledged user which is a member of the group 'rt'.  
+
+Configure your webserver to proxy requests to RT's 
+virtual directory to the apache instance you just set up.
+
+TODO: doc the apache configs needed to do this.
+
+The same technique can be used to run multiple RT2 instances on the same host.
+
+
diff --git a/rt/docs/design_docs/CARS b/rt/docs/design_docs/CARS
new file mode 100755 (executable)
index 0000000..a4d2a78
--- /dev/null
@@ -0,0 +1,66 @@
+Conditional Automated Request Shuffler 
+Initial Design.                <jesse@fsck.com> 9 Nov 99
+
+#Try to find out what queue the incoming ticket is in
+#Try to find out the default action for this invocation
+#Read the ticket from STDIN
+#Obtain the actor
+#Obtain the serial # if we have one
+#If the ticket has a ticket-id
+       #if this is a 'comment'
+               #add the current mime objects as a 'comment'
+
+       #if this is 'correspondence'
+               #add the current mime object as 'correspondence'
+
+
+#if this ticket does not yet have a ticket id
+
+  #For now:
+       #Create a new ticket
+
+  #In the distant future
+
+       #load the regexp table matching this queue
+       #check the message agains the regexp table, ordered by precedence
+               #when we get a match
+                       #get the ruleset for that regexp from the actions table
+                       #evaluate the ruleset in order of precedence.
+                          #if we get an 'exit' stop proccesing ALL rulesets
+wpw                       #if we get a 'forward,' forward it to 'value'.
+
+                          #if we get a 'create,' create a request in 'value'
+                          #elseif we get a 'map', add this as additional correspondence on ticket 'value'
+
+    
+                          #if we get an 'associate', associate the ticket number returned from the 
+                          'create' or 'map' with the master ticket from 'value'
+
+                          #if we get a 'reply', 
+                               #load the reply template with id 'value'      
+                               #replace strings in the template
+                               #send the template
+
+
+
+
+CREATE TABLE Rules {
+ID int AUTO_INCREMENT,
+Desc varchar(120),
+Regexp varchar(80),
+Precedence int,
+MatchField varchar(20), #Can be a headername or 'any' all header names
+                        #end in :
+
+
+CREATE TABLE Actions {
+Rule int,
+Action varchar(20), # Create, Forward, Squelch, Owner, Area, Associate
+Value varchar(20), #queue or email address
+Desc varchar(120)
+}
+
+CREATE TABLE Autoreplies {
+ID int AUTO_INCREMENT,
+Content text
+);
\ No newline at end of file
diff --git a/rt/docs/design_docs/TransactionTypes.txt b/rt/docs/design_docs/TransactionTypes.txt
new file mode 100755 (executable)
index 0000000..942b723
--- /dev/null
@@ -0,0 +1,48 @@
+This is some loose scrabbling made by TobiX, they might eventually be relevant
+for 2.1.
+
+INTERFACES, in general
+
+should:
+
+- provide the user (client?) with a list of possible actions (methods).
+- let the user execute those actions (methods).
+- Return information to the user/client.
+
+There are two kind of actions/methods:
+
+- Information retrieval
+- Transactions
+
+For the first, I think the best thing is to just provide a lot of
+methods for it in the libraries, and let it be an Interface Design
+Issue what to show and how to show it.
+
+For the second, I think we can win in the long run on having a
+generalized methods for 
+
+- listing transaction types.
+- creating & committing transactions.
+
+..with the possibility of just deploying new custom-developed modules
+when new transaction types are needed.
+
+
+$RT::TransactionTypes  ...and...
+%RT::TransactionTypes
+   - global object which contains all TransactionTypes
+   - used by all UIs to create menues of possible (user) actions (one TransactionType is a user action)
+
+The UIs should call sth like
+$Ticket->AddTransaction($TransactionName), which should be equivalent
+with i.e.  $Ticket->Correspond when $TransactionName is 'Correspond'
+(AUTOLOAD should call the do-sub if exists
+$RT::TransactionTypes{$TransactionName})
+
+The RT::Ticket::AddTransaction will create a new transaction of the
+right TransactionClass (maybe via a sub
+RT::TransactionTypes::NewTransaction).  Then $Transaction->do is
+called.
+
+TransactionType->do initializes a new object of the right TransactionClass, and 
+
diff --git a/rt/docs/design_docs/acls b/rt/docs/design_docs/acls
new file mode 100644 (file)
index 0000000..3b9d856
--- /dev/null
@@ -0,0 +1,206 @@
+$Header: /home/cvs/cvsroot/freeside/rt/docs/design_docs/acls,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+
+
+# {{{ Requirements 
+
+Here's the rough scheme I was thinking of for RT2 acls. Thoughts? I think
+it's a lot more flexible than RT 1.0, but not so crazily complex that
+it will be impossible to implement.  One of the "interesting" features
+is the ability to grant acls based on watcher status. This now lives
+in design-docs/acls
+
+        jesse
+
+Who can rights be granted to:
+
+       users whose id is <foo>
+       users who are watchers of type <requestor/cc/admincc> for <queue/ticket> <id>
+       users who are watchers of type <requestor/cc/admincc> for <this ticket / this queue>
+
+
+what scope do these rights apply to
+       queue <id>
+       system
+       
+
+What rights can be granted
+       Display Ticket
+       Manipulate Ticket
+               Only users with manipulate ticket level access will see comments
+       Maniplulate Ticket Status
+       Create Ticket   
+
+       Admin Queue Watchers 
+       Admin Ticket Watchers
+       Admin user accounts
+       Admin scrips
+       Admin scripscopes
+       Admin Queue ACLS
+       Admin System ACLs
+
+# }}}
+
+
+# {{{ Prinicpals  These are the entities in your Access Control Element
+#
+
+Principal: What user does this right apply to
+
+       Made up of: 
+               PrincipalScope, PrincipalType and PrincipalId
+
+       
+       User:   
+               Scope:  User    
+               Type:   null
+               Id:     A userid or 0
+
+       Owner:
+               Scope:  Owner
+               Type:   null
+               Id:     none
+
+
+       Watchers:
+
+               Scope: Ticket
+               Type:   Requestors; Cc; AdminCc
+               Id:     A ticket id or 0 for "this ticket"
+
+               Scope: Queue
+               Type:   Cc; AdminCc
+               Id:     A queue id or 0 for "this queue"
+
+
+# }}}
+
+# {{{ Object: What object does this right apply to
+
+       Object is composed of an ObjectType and an ObjectId
+
+       Type:   System  
+       Id:     NULL
+
+       Type:   Queue
+       Id:     Integer ref to queue id or 0 for all queues
+       
+# }}}
+
+# {{{ Right: (What does this entry give the principal the right to do)
+
+
+
+       For the Object System:
+               System::SetACL
+               System::AdminScrips
+
+               User::Display
+               User::Create
+               User::Destroy
+               User::Modify
+               User::SetPassword
+
+
+
+       For the Object "Queue":
+               Queue::Admin
+               Queue::SetACL
+               Queue::Create
+               Queue::Display
+               Queue::Destroy
+               Queue::ModifyWatchers
+               Ticket::Create
+               Ticket::Destory
+               Ticket::Display
+               Ticket::Update
+               Ticket::UpdateRequestors
+               Ticket::UpdateCc
+               Ticket::UpdateAdminCc
+               Ticket::NotifyWatchers
+
+               
+               DEFERRED
+
+               Ticket::SetStatus:      (Values)
+                                       Open
+                                       Resolved
+                                       Stalled
+                                       <null> means any
+
+
+# }}}
+
+
+# {{{ Implementation:
+
+# {{{ SQL Schema 
+CREATE TABLE ACL (
+       id int not null primary_key autoincrement,
+       PrinicpalId INT(11),
+       PrincipalType VARCHAR(16),
+       PrincipalScope VARCHAR(16),
+       ObjectType VARCHAR(16),
+       ObjectId  INT,
+       Right VARCHAR(16)
+);
+
+# }}}
+
+# {{{ perl implementation of rights searches
+
+sub Principals {
+if (defined $Ticket) {
+       return "($UserPrincipal) OR ($OwnerPrincipal) OR ($WatchersPrincipal)";
+       }
+else {
+       return   "($UserPrincipal) OR ($WatchersPrincipal)";
+       }  
+}
+       
+$Principals = " ($UserPrincipal) OR ($OwnerPrincipal) OR ($WatchersPrincipal)";
+
+$UserPrincipal = " ( ACE.PrincipalScope = 'User') AND 
+                  ( ACE.PrincipalId = $User OR ACE.PrincipalId = 0)";
+
+$OwnerPrincipal = " ( ACE.PrinciaplScope = 'Owner') AND 
+                     ( Tickets.Owner = "$User ) AND    
+                     ( Tickets.Id = $Ticket)";
+
+$WatchersPrincipal = " ( ACE.PrincipalScope = Watchers.Scope ) AND 
+                     ( ACE.PrincipalType = Watchers.Type ) AND 
+                     ( ACL.PrincipalId = Watchers.Value ) AND 
+                     ( Watchers.Owner = $User )";
+
+$QueueObject = "( ACE.ObjectType = 'Queue' and (ACE.ObjectId = $Queue OR ACE.ObjectId = 0)";
+
+$SystemObject = "( ACE.ObjectType = 'System' )";
+
+
+# This select statement would figure out if A user has $Right at the queue level
+
+SELECT ACE.id from ACE, Watchers, Tickets WHERE ( 
+            $QueueObject
+            AND ( ACE.Right = $Right) 
+            AND ($Principals))
+
+# This select statement would figure outif a user has $Right for the "System"
+
+SELECT ACE.id from ACE, Watchers, Tickets WHERE ( 
+            ($SystemObject) AND ( ACE.Right = $Right ) AND ($Principals))
+
+# }}}
+
+# }}}
+
+# {{{ Examples
+#
+
+# }}}  
+
+
+
+Unaddressed issues:
+
+       There needs to be a more refined method for grouping users, such that members of the customer service department
+can't change sysadmins' passwords.
diff --git a/rt/docs/design_docs/basic-definitions.txt b/rt/docs/design_docs/basic-definitions.txt
new file mode 100644 (file)
index 0000000..23d2c57
--- /dev/null
@@ -0,0 +1,54 @@
+(todo ... basically, those are untouched from 1.0)
+Ticket
+Queue
+(...more?)
+
+Requestor
+
+  (...definition of a requestor .. blahblah)
+
+  I'm often doing a distinction between "Internal Requestors" and "External
+  Requestors" (see below).  The system doesn't make any difference between
+  requestors, but the distinction might be useful to discuss usage patterns,
+  templates and configurations.
+
+
+External Requestor
+
+  Might be a customer or a potential customer.  The External Requestor
+  should be treated as a VIP.  (S)he shouldn't need to see too much of RT.
+  The support (s)he gets should be as personal as possible.  The external
+  requestor might eventually get access to the Web UI, but only to track
+  her/his own requests.  If you're not planning to use RT for handling
+  external customers, all your requestors are probably "Internal
+  Requestors".
+
+
+Watcher
+
+  Somebody that are "subscribing" to a queue or a ticket (or something
+  differently).  Basicly, somebody watching a queue or a ticket should get
+  all updates by email.  A requestor is a (special) watcher.
+
+
+Regular Watcher
+
+  People within the same organization, people that have read access to whole
+  queues.
+
+  I consider "Regular Watchers" as well as "Internal Requestors" as more
+  robust and capable human beeings than the fragile customers.  We don't
+  mind letting them get entagled with RT, and we let them access the Web UI.
+  They can live with beeing just the Cc or Bcc at an email.
+
+
+Internal Requestor
+
+  An Internal Requestor is usually internal to the company.  He might be 1st
+  line support sending matters to tech support or similar. Might be an
+  internal employee sending matters to tech support (or even 1st line
+  support if he's not sure where to send matters).  It might also be that
+  "ordinary" requestors actually might be treated as intelligent human
+  beeings rather than VIPs, i.e. in open source projects ... we'll still
+  call them "Internal Requestors" as they don't need the special VIP
+  treatment.
diff --git a/rt/docs/design_docs/cli_spec b/rt/docs/design_docs/cli_spec
new file mode 100644 (file)
index 0000000..48a7f34
--- /dev/null
@@ -0,0 +1,354 @@
+
+Find tickets to operate on:
+        --id=<tickets>          Find only tickets in the range <tickets>
+                synonyms:
+                --limit-id, --tickets, --limit-tickets
+        --limit-queue=<queue>
+        --limit-status=<status>
+        --limit-owner=<owner>
+        --limit-priority=<priority>
+        --limit-requestor=<email>
+        --limit-subject=<string>      (Subject contains)
+        --limit-body=<string>         (body contains)
+        --limit-created=(before/after) <date>
+        --limit-due=(before/after) <date>
+        --limit-starts=(before/after) <date>
+        --limit-started=(before/after) <date>
+
+        --limit-first=<int>           Start on the <int>th row returned by the
+                                database  
+        --limit-rows=<int>      Find only <int> rows
+
+Display:
+        --show                  shows a ticket history
+        --history               ditto
+
+        --summary               default option. shows a ticket summary
+                --format        Optional format string. If not specified,
+                                uses the value of ENV{'RT_LISTING_FORMAT'}
+                                or an internal default
+        
+
+Basic ticket editing:
+
+       --status=(open|stall|resolve|kill)
+       --subject=<string>
+        --owner=<owner>
+       --queue=<queue>
+       --time-left=<minutes>
+
+Watcher-related editing:
+
+       --add-requestor=<email>
+       --del-requestor=<email>
+       --add-cc=<email>
+       --del-cc=<email>
+       --add-admincc=<email>
+       --del-admincc=<email>
+
+Priority related editing:
+
+       --priority=<int>
+       --final-priority=<int>
+
+Date related editing:
+
+       --due=date
+       --starts=date
+       --started=date
+       --contacted=date
+
+
+
+Ticket updates:
+
+       --comment 
+       --reply | --respond 
+
+
+Links
+
+       --add-link 
+               --type=<DependsOn|MemberOf|RelatedTo>
+               needs one of:
+                  --target=<ticketid> 
+                  --base=<ticketid>
+
+       --del-link <link-id>
+
+
+
+Condiments:
+       any update can take:
+
+               --time-taken <minutes>
+
+       Ticket updates can take:
+
+               --source        -- specify a source file to read the content from
+               --edit          = give me an editor to edit the message
+               --no-edit       = don't give me an editor to edit the message.
+
+
+
+
+----- Forwarded message from deborah kaplan <deborah@curl.com> -----
+
+Date: Fri, 14 Apr 2000 11:43:18 -0400 (EDT)
+From: deborah kaplan <deborah@curl.com>
+To: Jesse <jesse@fsck.com>
+Subject: Re: [rt-devel] RT Projects list
+
+Finally, here is the functional spec for the command line
+interface.  This is for the user interface only; if you think
+this is right, I will add the administrative interface as well.
+Should I post to rt-devel, add to the ticket, or just modify
+based on your kibbitzing?  When you are happy with it I'll start
+the code.
+
+-deborah
+
+
+RT command line interface functional specification
+Author: Deborah Kaplan (Deborah@suberic.net)
+Version:0.1
+
+Requirements: 
+
+RT needs a CLI for various reasons.  If a user is restricted to a
+dumb terminal, she needs to be able to access the RT database and
+manipulate it fully.  The full functionality of both the RT
+database and the RT administrative interface should be available
+from this CLI.
+
+There are two possible types of CLI which I will discuss here.
+The first is a curses-style interface, which allows the user to
+move about a series of menus and choices, usually using arrow
+keys.  As RT supplies a Web interface, there is no need for this
+curses-style interface to be written as part of RT.  Instead, the
+RT developers should pick one tty-based Web browser (e.g. lynx,
+w3m) and make sure that all of the RT pages are easily readable
+with that tty based browser.  Installation of that browser should
+be recommended in the RT installation documentation as a
+supported method of accessing RT from a tty.
+
+The second possible type of CLI is more minimal: a series of
+commands which can be run at a UNIX command prompt which provide
+full functionality to the RT database and administrative
+interface.  There are two major benefits to this second type of
+CLI.  First of all, in order to use this CLI, you need no extra
+tools (Web browsers, etc.).  All that is required is a UNIX
+command line prompt and an installation of RT.  Secondly, a user
+of RT who has a very specific command to run and who knows the
+appropriate CLI commands can accomplish her task much more
+quickly with a single command then she could navigating through a
+menu based interface.
+
+In the specification, I will describe the second type of CLI.
+
+Caveats:
+
+This specification draws heavily on the structure of formatting
+command line options for cvs.  RT faces a smaller version of the
+same kinds of problems cvs faces: we want to create a very rich
+command set without sacrificing ease-of-use. 
+
+I am not wedded to any specific command names if they seem
+impractical; I merely am proposing the command names that seem
+reasonable to me at this moment.
+
+Finally, I am finding the functioning of the web UI from RT 1.
+If the functionality differs greatly in RT 2, I will need to
+modify this specification.
+
+Specification:
+
+There are two commands: "rt", which is the primary interface to
+the database, and "rtadmin", which is the administrative
+interface to the database.
+
+The format of an rt command is as follows:
+
+ rt <command> 
+    <command> is one of:
+
+    - help
+      print an overview of the commands which can be run
+
+    - print <queue> <options> 
+      with no options, dump to the screen a list of all open
+      requests in <queue> -- the equivalent of "Display Queue" in
+      the existing Web interface.
+
+      <queue> is the name of an RT queue
+      <option> is either:
+
+        -f <filename> |  --filename <filename>
+          where <filename> is the name of a file (defaulting to
+          ~/.rtrc) in which the options described below can be
+          placed in the format "^ <long option name> <option value>
+          $".
+
+        Or a series of the following options:
+
+        -o <owner name> |  --owner <owner name>: restrict tickets
+       viewed to those owned by <owner name>.
+          This option can be used multiple times in one call of
+          the rt command in order to produce a list which
+          contains tickets owned by multiple owners.  Giving the
+          empty string ("") as an option to this switch will
+          restrict tickets viewed to those which have no owner.
+          If this switch is given with no argument, the option
+          defaults to the user name of the currently running
+          process.
+
+        -r <requestor name> | --requestor <requestor name>:
+       restrict tickets viewed to those requested by <requestor
+       name>.
+          This option can be used multiple times in one call of
+          the rt command in order to produce a list which
+          contains tickets requested by multiple requesters.  If
+          this switch is given with no arguments, it produces an
+          error.
+
+        -s <status> | --status <status>: restrict tickets viewed
+       to those with the status named in <status>.
+          This option can be used multiple times in one call of
+          the rt command in order to produce a list which
+          contains tickets with multiple statuses (statii?
+          Dragon NaturallySpeaking recognizes "statuses" as a
+          word).  This option defaults to status "open".
+
+        -j <subject> | --subject <subject>: restrict tickets
+       viewed to those which contain <subject> as a substring in
+       the subject field of the ticket.
+          This command can be used multiple times in one call of
+          the rt command in order to produce a list which
+          contains tickets with various subject substrings.  If
+          the option is called with no argument, the result is
+          an error.
+
+        -h | --help: print a usage message.
+
+        -n | --number: print out a specific ticket.
+          This command can be used multiple times to produce a
+          list which contains multiple tickets.  If the option
+          is called with no argument, the result is an error.
+
+    This completes all of the print options which are available
+    in the Web interface, except the sort options.  I maintain
+    that this command is already excessively complex, and that
+    adding functionality which can be replicated easily by
+    standard UNIX tools is unnecessary added complexity.  I
+    recommend that the man pages and documentation for this
+    option contain an example of a command line run (e.g. of rt |
+    awk) which replicates the sorting functionality provided by
+    the Web interface.
+
+    - edit <ticket> <options>
+      with no options, or with no <ticket>, produces the same
+      output as the --help option.
+      Otherwise, edits the ticket with number <ticket> as
+      indicated in the options given.  All options listed below
+      except for --help and --number can be used in conjunction
+      with one another to change many features of the same ticket
+      all at once.
+
+        -h | --help: print usage message
+
+        -s <status> | --status <status>: change the status to the
+       status listed in <status>.
+          No <status> listed, or 1 listed it does not come from
+          a list of approve statuses, produces an error.
+
+        -o <owner name> |  --owner <owner name>: set to the owner
+       of the ticket the owner named.
+          Follows whatever convention is finally decided on for
+          the requirement to steal a ticket that is owned by
+          somebody else.  No <owner named> listed has the user
+          who is running the rt program take the ticket.  If
+          that user is not a valid owner, or the 1 listed does
+          not come from a list of approve names, produces an
+          error.
+
+        -r <requestor name> | --requestor <requestor name>: sets
+       the requestor to <requestor name>.
+          Follows any conventions that the Web UI follows to
+          make sure that this is a legal name.  If not legal, or
+          left blank, produces an error.
+
+        -j <subject> | --subject <subject>:  sets the subject of
+       the ticket to <subject>.
+          If the option is called with no argument, the result
+          is an error.
+
+        -n <number one> <number 2> | --number <number one>
+       <number 2>: merges ticket number <number one> into ticket
+       <number 2>.
+          If both arguments are not provided, the result is an
+          error.
+
+        -q <queue> | --queue <queue>: set the queue to that
+       named.
+          If <queue> is not listed, or the 1 listed does not
+          come from a list of approve queues, produces an
+          error.
+
+        -a <area> | --area <area>: set the area of the ticket to
+       that named.
+          If <area> is not listed, or the 1 listed does not come
+          from a list of approve areas, produces an error.
+
+        -c <time stamp> | --contact <time stamp>: sets the last
+       user contact field, and produces an error if the format
+       is invalid.
+          If the argument is left blank, sets the last user
+          contact field to now.
+
+        -p <priority> | --priority <priority>: sets the current
+       priority to the 1 listed.
+          Produces an error if the argument is left blank.
+
+        -f <priority> | --final <priority>: sets the final
+       priority to the 1 listed.
+          Produces an error if the arguments left blank.
+
+        -d <date due> | --datedue <date due>: sets the due date
+       to the 1 listed.
+          Produces an error if the argument is left blank, or if
+          the format is invalid.
+
+    - comment <options>
+      with no options, this command reads from standard input
+      until it sees EOF and appends that to the ticket as a
+      comment.
+
+        -h | --help: print usage message
+
+        -c | --comment: append as a comment.  This is the default behavior.
+
+        -r | --reply: append as a reply.
+
+        -f <filename> | --file <filename>: can be used with
+       either the comment or reply options.  Instead of reading
+       from standard input, read the text of the comment or
+       reply from the file <filename>.
+
+    - report <options>
+      this command is a place holder for reporting functionality
+      which does not yet exist.  It will probably have the
+      default behavior to select reports at the command line or
+      choose default reports from a .rtrc file.  In a future
+      version, it can output graphs in some graphical format.
+
+
+
+----- End forwarded message -----
+
+-- 
+jesse reed vincent -- root@eruditorum.org -- jesse@fsck.com 
+70EBAC90: 2A07 FC22 7DB4 42C1 9D71 0108 41A3 3FB3 70EB AC90
+
+"If IBM _wanted_ to make clones, we could make them cheaper and faster than
+anyone else!"  - An IBM Rep. visiting Vassar College's Comp Sci Department.
+
diff --git a/rt/docs/design_docs/cvs_integration b/rt/docs/design_docs/cvs_integration
new file mode 100644 (file)
index 0000000..35c8737
--- /dev/null
@@ -0,0 +1,164 @@
+jesse@FSCK.COM: ok. anyone here
+       interested in having RT as a bug tracker integrated with CVS? ()
+
+marc: in principle, sure. ()
+
+jesse@FSCK.COM: want to write up your
+       ideal of how such a beast would work? ()
+
+alex_c: what sort of integration are you thinking of, Jesse? ()
+
+jesse@FSCK.COM: well, that's what I want
+       to know, alex. lots of people want their bug trackers tied to their
+       version control. I want to know what people want it to _do_ ;) ()
+
+alex_c: weird. :) ()
+
+marc: similarly to what the debian bts does. 
+       you put a magic string ("rt-closes#123") and it causes the bug in rt to
+       be closed (or appended with a different magic string) with the commit
+       message.  also nice would be if rt would then generate links to a
+       webcvs server. ()
+
+jhawk: Hrmm. cvs front-end that strips 'em out?
+       Perhaps with RT:  lines instead of CVS: lines in the commit
+       interaction? ()
+
+marc: the magic string goes in the commit
+       message, that is. no, use one of the magic post-commit scripts. ()
+
+
+jesse@FSCK.COM: well, there's also the
+       pre-commit script to lock out commits wihtout a ticket id ()
+
+jhawk: Personally, I don't want to force special
+       magic strings to the bug-tracking system, some of which may be
+       confidential, to appear in the cvs logs. ()
+
+marc: I could see wanting that on a release
+       branch. ()
+
+jhawk: I also think it would be cool to supply
+       template stuff for you to edit. ()
+
+jesse@FSCK.COM: I'm not sure cvs can be
+       made to do that. can it? (generate templates) ()
+
+jhawk: It would be reasonable, in my model, to
+       turn some kinds of RT: lines into things that fell in the commit
+       message, but not all kinds. ()
+
+marc: I don't quite see jhawk's objection. ()
+
+ghudson: In my observation, locking out commits
+       without a ticket ID is usually an impediment to development, and leads
+       to developers having the one bug which all commits cite. ()
+
+jhawk: If you had a CVS frontend, it could geneate
+       the template and feed it to 'cvs commit -m' ()
+
+ghudson: CVS can generate templates and verify
+       that they have been filled in. ()
+
+jhawk: What Greg says sounds cool; greg, what do
+    you mean? marc: one sec. ()
+
+marc: I think assuming a frontend is a terrible
+       idea. ()
+
+jesse@FSCK.COM: greg: agreed. but people
+       seem to want it. the idea would be only for a locked down release
+       branch. ()
+
+jhawk: marc: So, I might want to close an open
+       ticket as part of a commit message without that showing up in the
+       coommit message. Or to insert a splufty long comment into a ticket
+       while I do the commit but not close or really change the state, and
+       that comment might want to ramble a lot but not include that ramble in
+       the commit message. ()
+
+jesse@FSCK.COM: well, then arguably, you
+       might want to not use the commit message for that update, but instead
+       just go straight to the bts ()
+
+marc: I think the idea is to force you to
+       mention the ticket closing in the commit message. ()
+
+jesse@FSCK.COM: but yeah, state changing
+       and 'update messages' are seperate concepts that should both be
+       supported. ()
+
+jesse@FSCK.COM: part of the idea is to
+       drag the commit message into the BTS ()
+
+jhawk: Err, I think it quite frequent that I want
+       to put seperate info in both the commit message and the ticket system,
+       and entering them at the same time seems cool. ()
+
+jesse@FSCK.COM: ok. noted. I'll see if
+       that's doable, when i get around to this. ()
+
+marc: so I think you want a custom front-end,
+       but I don't think what you want is what jesse is talking about. ()
+
+jesse@FSCK.COM: the thing that would be
+       really cool that scare the pants off me is tracking which branches bugs
+       exist in / are fixed in ()
+
+jesse@FSCK.COM: what jhawk wants should
+       be doable, now that I understand his reqts. ()
+
+marc: that would require the bts to understand
+       branches in some fundamental way. ()
+
+jesse@FSCK.COM: yes. see above, about
+       the pants. ()
+
+sly: uh oh, not more people losing their pants... ..
+
+
+ghudson: RT needs to know the names of branches
+       and their structure (so that you can tell it "fixed in foo" and it
+       knows that the bug is still fixed in anything that branches off of foo,
+       but not necessarily in other new branches), but nothing more than that.
+
+jhawk: So, note that what I'm describing is how
+       I'd like the UI to be, from a generic architectural level, and not
+       really thinking terribly specific. Greg, can you explain the CVS
+       template thing? ()
+
+jesse@FSCK.COM: and it needs to know
+       exactly "when" a branch happens. because "fixed in foo" won't fix
+       something that branched off foo yesterday ()
+
+marc: jesse was talking about integrating rt
+       with cvs.  building a new developent+repository+bts from scratch would
+       be a problem with larger scope :-) ()
+
+jesse@FSCK.COM: marc: was that in
+       response to jhawk? ()
+
+ghudson: CVS and templates: "rcsinfo" lets you
+       specify a template for log messages, and "commitinfo" lets you check
+       them. ()
+
+ghudson: Er, sorry, my bad. 
+       s/commitinfo/verifymsg/ ()
+
+marc: with cvs, if you have the revision number
+       of the fix (which you should). you can use the branch version number to
+       get a date and see when the branch happened relative to the fix. ()
+
+marc: jesse: yes. ()
+
+jesse@FSCK.COM: Ok. would people
+       consider "integration with CVS" to be subpar or incomplete if it didn't
+       deal with tracking branches? ()
+
+marc: incomplete relative to an ideal, but not
+       subpar, as it would still be useful. ()
+
+allbery@CS.CMU.EDU: CVS's branch
+       support sucks so much that failure to work with it is hardly a bug ()
+
+
diff --git a/rt/docs/design_docs/evil_plans b/rt/docs/design_docs/evil_plans
new file mode 100644 (file)
index 0000000..34b9f81
--- /dev/null
@@ -0,0 +1,167 @@
+Current planned 2.2 feature list. subject to change.
+
+
+Core
+
+Should Make "Owner" a watcher type, rather than a special ticket attribute,
+       under the hood.  This wins for ACL and code consistency reasons.
+
+Web UI
+
+Should New "Tools" top level menu
+Should         "This week in RT" at a glance.
+Nice           "RT Stats" overview.
+Nice   recent and favorite items
+
+
+per-user configuration
+
+Must           Saveable user preferences.
+
+               The ideal implementation would be "saveable user metadata",
+               including things like "Alternate Email Addresses".  To 
+               do this right, not all user metadata would be directly
+               editable by the user who has "ModifySelf"  it may be that
+               this is a "system" datastore that gets accessed by various
+               functions, some of which the user has access to modify and
+               some of which only the system does.
+               
+               API:    Set field "FOO" to value "BAR" for user BAZ
+                       What values does field "FOO" have for user BAZ?
+                       Clear all values of "FOO" for user BAZ
+                       What users have value "BAR" for field "FOO"
+
+               Example usages:
+
+                       What users have the alternative email address matching 
+                       "boo@fsck.com"
+                       What custom searches does user BAZ have defined?
+                       What is baz's default queue?
+
+               Actually, I feel a little sketchy about Alternative Email 
+               Addresses in there. I'm not quite sure why yet.
+
+               The same would really be useful for queues. Damn it. I think
+               I want a registry.
+
+
+
+Searching
+
+Must   Ability to define search result format.
+should         Saveable user searches.
+nice   Sharable searches.
+
+
+Scrips
+
+must   Include more Conditions; at least those contributed so far
+       that make sense in my grand scheme of things
+
+should The name should change to something that people don't think is
+       spelled wrong.  ("I will not invent words\n" x 1000)
+
+nice   Scrips could apply to a list of queues, rather than just one queue or 
+       all of them.
+
+
+Custom fields
+
+Must   "KeywordSelects" become "Custom Fields"
+Should String and multi-string custom fields.
+Nice   Date custom fields
+Nice   Some way to order and group custom fields.
+Nice   Default values
+Nice   Required values
+Nice   Make custom fields apply to an enumerated list of queues, 
+       rather than just one. 
+
+
+Web infrastructure
+
+Must   Full fastcgi support.
+
+
+Installation 
+
+Should Better FSSTD conformance:
+               bins in /bin
+               admin tools in /sbin (does this include rtadmin?)
+               ephemeral data in /var
+               rename config file      
+               force local RT search path?     
+
+Mail gateway
+
+must   Integrate gpg-authenticated command-by-mail mode
+       
+
+
+Core
+
+should Use apache logging, if available
+should Use syslog, if available.
+should Mail user new password, as an Action, so it can be invoked either 
+       as a scripaction or from the web ui.
+
+
+
+Web Services Framework
+
+Should Expose an API to create a ticket by HTTP posting an XML document.
+Should  Provide an RSS feed to display tickets matching certain criteria
+Nice   Allow ticket updates via the web ui
+Nice   Export full ticket metadata and history as XML
+
+Note:  I currently favor the REST philosophy that GET and POST to specific,
+       defined URLs provides everything one needs to build comprehensive
+       web services without the massive added complexity of a SOAP or XML-RPC
+       framework.
+
+
+ACLs:
+
+Wish   New ACL primitives for:
+
+               List all users who have right "FOO" on object "BAR"
+               List all rights user "BAZ" has for object "BAR"
+               List all objects for which user "BAR" has right "FOO"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+For the near future:
+
+       Use case:
+               Jesse wants to get notified of all tickets in queue 'RT Bugs'
+               with a severity of 'critical' and also have a requestor whcih matches 'fsck.com'.  
+               I'm not sure this is the best idea.
+
+
+       Site admins define a number of subscriptions and can sign up individual
+       users, groups or metagroups to get mail on that subscription.
+
+       Basically, an admin would define "On Condition, notify as comment with
+       template _template_"
+
+       There would be a new table called "subscriptions"(?) that would have
+       the structure:
+
+               id
+               ScripId
+               PrincipalType  ENUM: User, Group, Owner, Requestors, AdminCcs, Ccs
+               PrincipalId --  UserId or GroupId.  For Owner, Requestors, AdminCcs, Ccs, it doesn't really make a lick of difference.  
diff --git a/rt/docs/design_docs/link-definitions.txt b/rt/docs/design_docs/link-definitions.txt
new file mode 100644 (file)
index 0000000..30b9035
--- /dev/null
@@ -0,0 +1,143 @@
+For 2.0, those Linking actions should be supported:
+
+1. DependentOn; TobiX-style.
+
+ BASE is dependent on TARGET.
+ ...meaning that TARGET has to be resolved before BASE (really) is
+ resolved.
+ According to TobiX, those "weird action" makes sense:
+ ...when the link and/or TARGET is created, the BASE might be stalled.
+ Alternatively, this should be very trivial to request through the UIs.
+ ...when the TARGET is resolved, BASE will be reopened if it's stalled.
+
+ An alternative to those "weird actions" is to have some run-time logic that
+ takes care of this; i.e. letting the search interface handle "please hide
+ all requests with unresolved dependencies"
+ TobiX will need to make dependency links into Bugzilla.
+
+ Dependency links should be made when more work to BASE should be done
+ after the TARGET is resolved and/or BASE can't be resolved before TARGET is
+ resolved.
+
+ Dependency links are often 1:1, but n:n links makes sense; one ticket can
+ depend on several others, several tickets can depend on one ticket, etc.
+
+ Loops don't make sense at all, but the system above won't break if it
+ encounter loops.
+
+ Dependency links is more for workflow than anything else.  When a new
+ TARGET is created, some of the work might be passed over to another
+ department/person ... but _not_ the responsibility for the communication
+ with the external requestor.
+
+2. MemberOf link (grouping)
+
+ BASE is a member of TARGET.
+
+ TobiX-style "weird actions":
+ ...when TARGET is beeing replied to, all BASE requestors should get the
+ reply.
+
+ ...when TARGET is resolved, all BASE tickets should be resolved (unless
+ they have other unresolved Dependencies/MemberOf links).
+
+ ...when all BASEs to one TARGET are resolved, TARGET should be resolved.
+ The alternative is to let the user choose "reply to all" and "resolve all"
+ through the user interfaces.
+
+ MemberOf should be used when BASE ticket states more or less the same as
+ the TARGET ticket, and we do want to give a reply to all requestors, but we
+ don't want to merge them (Individual tickets from individual external
+ requestors should be respected as separate entities). If BASE tickets from
+ more than one external requestor is linked to a TARGET ticket, we denote
+ the TARGET ticket as a "Group ticket".  This is only a documentation
+ definition, you won't find any references to "Group tickets" in the source.
+
+ I think the proper etiquette should be to clearly state in a reply to a
+ group ticket that the mail is going to several persons, and that the
+ requestor should reply back if they feel their Ticket hasn't got the
+ attention it deserves.  The user documentation should reflect this.
+
+ MemberOf links can also be used to hand away the work flow. The person in
+ charge of the TARGET ticket will also be in charge of the BASE tickets and
+ the communication with the end user.
+
+ If a work task needs to be splitted into two subtasks, MemberOf might also be
+ used.
+
+ 1:n links makes more sense, but n:n can also work in some cases.
+ The reply stuff might break seriously upon loops.  Recursement might be
+ handy for splitting a work task into subtasks (making a hierarchical tree
+ of the worktasks).
+
+
+3. Merge (connecting)
+
+ BASE is the same as TARGET.
+
+ ...the system should somehow merge together transactions for both tickets.
+ ...BASE should be more or less deleted, only the TARGET should apply.
+ ...actions done toward BASE should be redirected to TARGET.
+
+ I think MergeLinks should be used when two tickets accidentally has
+ appeared twice in the system, and/or there is no reason to keep the two
+ tickets separately.  It might be that it's the same requestor (i.e.
+ clicking the "send" button twice in a web environment) or that we don't
+ care much about giving the requestor individual follow-up (typically
+ "internal" requestors, etc.)
+
+ Based on user feedback, merged tickets will be displayed as the same ticket
+within RT's user interfaces. but the original tickets' transactions will be
+kept seperated in the database. this may require some magic.
+4. RefersTo / No Action link (linking)
+
+ BASE is somewhat related to TARGET
+
+ No special actions will be taken.
+ Loops might maybe make sense
+
+BASE and TARGET are usually Tickets within one RT instance, but it
+might also point to external RT instances, other DB systems, etc.
+
+
+
+
+In future revisions, it should be very easy to set up site-specific link action types.
+We should also consider to include more linking actions in the box.
+
+An example stolen from John Rouillard.  Eventually the [comments] should be
+removed, and the text modified to fit the planned 2.0 link actions:
+
+  ticket  problem
+    1         can't connect to hosts with netscape
+    2         ping is broken
+    3         Can't send email: error no space on spool/mqueue
+         
+  You have the above in the queue. You realize that DNS is down. Spawn
+  a ticket
+         
+    4         DNS is down
+
+  mergelink 1 and 2 to it [I would rather say "make a MemberOf link _or_ a
+  dependency link from 1 and 2 to 4" --TobiX] (if you choose to stall 1 and
+  2 automatically feel free, its just a shell script change) [well, you
+  might choose dependency instead of MemberOf --TobiX]. The person working
+  on 3 has come to the conclusion that outgoing mail is backing up because
+  of the DNS failure. She has cleared space by copying the mail queue to
+  another disk, but can't really get email working till DNS is up. So she
+  creates a Dependency linkon  ticket 4 stalling ticket 3.
+
+  We finally get DNS working and resolve ticket 3. What happens?  Tickets 1
+  and 2 are resolved and email is sent to requestors notifying them of the
+  resolution [This is the default behaviour for 2.0 MemberOf-linked tickets.
+  Remember that if we send Replies to "Group Tickets" (that is, the target
+  of several "MemberOf" links) --TobiX]. Ticket 4 [should be 3? --TobiX] is
+  reopened and the person working on it starts flushing the mail queue and
+  the moved mailq by hand. 
+
diff --git a/rt/docs/design_docs/local_hacking b/rt/docs/design_docs/local_hacking
new file mode 100644 (file)
index 0000000..c06d112
--- /dev/null
@@ -0,0 +1,32 @@
+To facilitate local hacking, RT needs a mechanism to allow site administrators
+to easily add HTML templates for the web ui and to replace sections
+of code in RT's core modules _without_ having to modify those modules
+
+We'll use several methods to achieve this goal.
+
+       Webui
+               HTML::Mason allows users to create multiple
+component hierarchies.  RT should ship with a local component root
+defined and available. This root should be configured as the "primary"
+component root.
+
+
+       Core modules
+
+       This gets a bit trickier. we want to allow people to trivially
+subclass core modules and to use those subclasses throughout the code.
+
+The way we're going to handle this is by setting up a number of subroutines
+in config.pm that look something like this:
+
+sub NewTicketObj {
+       eval "require $TicketClass";
+       my $object = new $TicketClass;
+       return ($object);
+}
+
+# This variable is used for ref type checking
+$TicketClass = "RT::Ticket";
+
+we could use an eval around the require and thus completely avoid specifying
+the object in two places. which feels like a win. but i'm worried about perf.
diff --git a/rt/docs/design_docs/subscription-definitions.txt b/rt/docs/design_docs/subscription-definitions.txt
new file mode 100755 (executable)
index 0000000..deda35c
--- /dev/null
@@ -0,0 +1,113 @@
+NEW SCRIP NOTES
+
+
+RT Actions:
+
+
+   EmailOwnerAsComment
+       Send mail to the ticket owner from the queue's comment address
+       
+   EmailOwnerOrAdminWatchersAsComment
+       Send mail to the ticket owner, or if there is no owner, the ticket's admin watchers
+       from the queue's comment addresses
+
+   EmailAdminWatchersAsComment
+       Send mail to the ticket's adminstrative watchers from the queue's comment address
+
+
+
+   EmailOwner
+       Send mail to the ticket owner from the queue's correspond address
+       
+   EmailOwnerOrAdminWatchers
+       Send mail to the ticket owner, or if there is no owner, the ticket's admin watchers
+       from the queue's correspond addresses
+
+   EmailAdminWatchers
+       Send mail to the ticket's adminstrative watchers from the queue's correspond address
+
+   EmailWatchers
+       Send mail to the ticket watchers from the queue's correspond address
+
+   AutoReply 
+       Sendmail to the requestor from the queue's correspond address.
+            
+   
+
+RT Conditions:
+   OnCreate
+   OnEachTransaction
+   OnComment
+   OnCorrespond
+   
+
+
+
+
+What is an Action?
+
+...some piece of code that can do something whenever a transaction is done.
+The actions shipped with RT sends email and can handle some logic that makes
+sense for some instances.  site-specific modules can be dropped in to
+perform special actions.
+
+
+What can an Action do?
+
+- decide whether it's applicable or not
+- prepare
+- commit
+- describe itself
+
+...and if it's a subclass of SendEmail, you can also override a lot.
+
+Currently the schema.mysql contains a list of the basic subscription-related
+actions that will be bundled with RT.
+
+
+What is a Scrip?
+
+...it's an entry in the database that tells that an action is to be
+performed with a certain template and argument.  Template and argument
+doesn't make sense in all contexts.  A scrip can be limited to transaction
+types; the current implementation allows a comma-separated list (though for
+a "cleaner" schema design, it should be a separate table for this?).  It has
+a name and a description.
+
+
+What is a ScripScope?
+
+...an indication of what queues the different Scrips applies to.  It should
+be easy to remove/insert ScripScope objects by the admin tools.
+
+
+What is a Watcher?
+
+...it's a request for beeing kept updated on a ticket and/or a queue
+and/or whatever.  It is to be used by the Actions.  Watcher items can
+easily be enabled/disabled through the `Quiet' attribute.  `Type' might
+indicate what emails the watcher wants to get and how to get them.
+
+The Bcc/Cc watchers should be handled by the NotifyWatchers action which is
+run regardless of the Scrips.
+
+
+What is a Template?
+
+...A template is a text template that is to be used for outgoing email -
+or for different use for different actions.  One template can be used by
+several Scrips.
+
+
+How does the system determinate whom to send mail to?
+
+The ScripScope table in the DB should indicate whether a Scrip is relevant
+for a queue or not /* TobiX thinks that this might eventually be extended to
+keywords, tickets, etc, and not only Queues */ ... the Scope table should
+indicate whether the Scrip is relevant for a given transaction type ... then
+the given Action should determinate whether it applies or not, and finally
+the Action has to find out (via the Watchers table) whom it applies to, and
+how to contact them ... and the Template tells how the mails that are sent
+out should look like.
+
+
diff --git a/rt/docs/design_docs/users b/rt/docs/design_docs/users
new file mode 100644 (file)
index 0000000..71c4476
--- /dev/null
@@ -0,0 +1,14 @@
+RT2 makes everybody a user. some sites won't like this. there
+should be away to make an "anonymous" user who the mailgate makes
+the requestor for all mailed in tickets. it would then set the
+ticket 'requestor' watcher's alternate email address to the real
+requestor's email.
+
+additionally, eventually, users will need to be deleted. RT doesn't
+want any user deleted. Instead, there will be a flag in the user's
+entry in the users table called 'Disabled.'  Disabled users will
+not be able to be granted rights.
+
+       The process of disabling a user should remove their acls and
+should force the giving away of their tickets or reject the disabling.
+
diff --git a/rt/docs/rt.gif b/rt/docs/rt.gif
new file mode 100755 (executable)
index 0000000..693b062
Binary files /dev/null and b/rt/docs/rt.gif differ
diff --git a/rt/etc/acl.Oracle b/rt/etc/acl.Oracle
new file mode 100644 (file)
index 0000000..59d35a0
--- /dev/null
@@ -0,0 +1,9 @@
+CREATE USER !!DB_RT_USER!! identified by !!DB_RT_PASS!!
+temporary tablespace TEMP
+default tablespace USERS
+quota unlimited on USERS;
+
+grant connect, resource to !!DB_RT_USER!!;
+
+exit;
+
diff --git a/rt/etc/acl.Pg b/rt/etc/acl.Pg
new file mode 100755 (executable)
index 0000000..13ac41d
--- /dev/null
@@ -0,0 +1,39 @@
+drop user !!DB_RT_USER!!;
+create user !!DB_RT_USER!! with password '!!DB_RT_PASS!!' NOCREATEDB NOCREATEUSER;
+
+grant select, insert, update, delete on Groups to !!DB_RT_USER!!;
+grant select, insert, update, delete on Groups_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on ACL to !!DB_RT_USER!!;
+grant select, insert, update, delete on ACL_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on Watchers to !!DB_RT_USER!!;
+grant select, insert, update, delete on Watchers_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on Links to !!DB_RT_USER!!;
+grant select, insert, update, delete on Links_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on Users to !!DB_RT_USER!!;
+grant select, insert, update, delete on Users_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on Tickets to !!DB_RT_USER!!;
+grant select, insert, update, delete on Tickets_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on GroupMembers to !!DB_RT_USER!!;
+grant select, insert, update, delete on GroupMembers_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on Queues to !!DB_RT_USER!!;
+grant select, insert, update, delete on Queues_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on Transactions to !!DB_RT_USER!!;
+grant select, insert, update, delete on Transactions_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on ScripActions to !!DB_RT_USER!!;
+grant select, insert, update, delete on ScripActions_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on ScripConditions to !!DB_RT_USER!!;
+grant select, insert, update, delete on ScripConditions_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on Scrips to !!DB_RT_USER!!;
+grant select, insert, update, delete on Scrips_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on Attachments to !!DB_RT_USER!!;
+grant select, insert, update, delete on Attachments_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on Templates to !!DB_RT_USER!!;
+grant select, insert, update, delete on Templates_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on Keywords to !!DB_RT_USER!!;
+grant select, insert, update, delete on Keywords_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on ObjectKeywords to !!DB_RT_USER!!;
+grant select, insert, update, delete on ObjectKeywords_id_seq to !!DB_RT_USER!!;
+grant select, insert, update, delete on KeywordSelects to !!DB_RT_USER!!;
+grant select, insert, update, delete on KeywordSelects_id_seq to !!DB_RT_USER!!;
+
+
diff --git a/rt/etc/acl.mysql b/rt/etc/acl.mysql
new file mode 100755 (executable)
index 0000000..7feb376
--- /dev/null
@@ -0,0 +1,4 @@
+
+DELETE FROM user WHERE user like '!!DB_RT_USER!!';
+DELETE FROM db where db LIKE '!!DB_DATABASE!!';
+GRANT SELECT,INSERT,CREATE,INDEX,UPDATE,DELETE ON !!DB_DATABASE!!.* TO !!DB_RT_USER!!@!!DB_RT_HOST!! IDENTIFIED BY '!!DB_RT_PASS!!';
diff --git a/rt/etc/config.pm b/rt/etc/config.pm
new file mode 100755 (executable)
index 0000000..52b1a0b
--- /dev/null
@@ -0,0 +1,473 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/etc/Attic/config.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $      
+
+package RT;
+
+# {{{ Base Configuration
+
+# $rtname the string that RT will look for in mail messages to
+# figure out what ticket a new piece of mail belongs to
+
+# Your domain name is recommended, so as not to pollute the namespace.
+# once you start using a given tag, you should probably never change it. 
+# (otherwise, mail for existing tickets won't get put in the right place
+
+$rtname="example.com";  
+
+# You should set this to your organization's DNS domain. For example,
+# fsck.com or asylum.arkham.ma.us. It's used by the linking interface to 
+# guarantee that ticket URIs are unique and easy to construct.
+
+$Organization = "example.com";
+
+# $user_passwd_min defines the minimum length for user passwords. Setting
+# it to 0 disables this check
+$MinimumPasswordLength = "5";
+
+# $Timezone is used to convert times entered by users into GMT and back again
+# It should be set to a timezone recognized by your local unix box.
+$Timezone =  'US/Eastern'; 
+
+# LogDir is where RT writes its logfiles.
+# This directory should be writable by your rt group
+$LogDir = "!!RT_LOG_PATH!!";
+
+# }}}
+
+# {{{ Database Configuration
+
+# Database driver beeing used - i.e. MySQL.
+$DatabaseType="!!DB_TYPE!!";
+
+# The domain name of your database server
+# If you're running mysql and it's on localhost,
+# leave it blank for enhanced performance
+$DatabaseHost="!!DB_HOST!!";
+
+# The port that your database server is running on.  Ignored unless it's 
+# a positive integer. It's usually safe to leave this blank
+$DatabasePort="!!DB_PORT!!";
+
+
+#The name of the database user (inside the database) 
+$DatabaseUser='!!DB_RT_USER!!';
+
+# Password the DatabaseUser should use to access the database
+$DatabasePassword='!!DB_RT_PASS!!';
+
+
+# The name of the RT's database on your database server
+$DatabaseName='!!DB_DATABASE!!';
+
+# If you're using Postgres and have compiled in SSL support, 
+# set DatabaseRequireSSL to 1 to turn on SSL communication
+$DatabaseRequireSSL=undef;
+
+# }}}
+
+# {{{ Incoming mail gateway configuration
+
+
+# OwnerEmail is the address of a human who manages RT. RT will send
+# errors generated by the mail gateway to this address.  This address
+# should _not_ be an address that's managed by your RT instance.
+
+$OwnerEmail = 'root';
+
+# If $LoopsToRTOwner is defined, RT will send mail that it believes 
+# might be a loop to $RT::OwnerEmail 
+
+$LoopsToRTOwner = 1;
+
+# If $StoreLoopss is defined, RT will record messages that it believes 
+# to be part of mail loops.
+# As it does this, it will try to be careful not to send mail to the 
+# sender of these messages 
+
+$StoreLoops = undef;
+
+
+# $MaxAttachmentSize sets the maximum size (in bytes) of attachments stored
+# in the database. 
+
+# For mysql and oracle, we set this size at 10 megabytes.
+# If you're running a postgres version earlier than 7.1, you will need
+# to drop this to 8192. (8k)
+
+$MaxAttachmentSize = 10000000;  
+
+# $TruncateLongAttachments: if this is set to a non-undef value,
+# RT will truncate attachments longer than MaxAttachmentLength. 
+
+$TruncateLongAttachments = undef;
+
+
+# $DropLongAttachments: if this is set to a non-undef value,
+# RT will silently drop attachments longer than MaxAttachmentLength. 
+
+$DropLongAttachments = undef;
+
+# If $ParseNewMessageForTicketCcs is true, RT will attempt to divine
+# Ticket 'Cc' watchers from the To and Cc lines of incoming messages
+# Be forewarned that if you have _any_ addresses which forward mail to
+# RT automatically and you enable this option without modifying 
+# "IsRTAddress" below, you will get yourself into a heap of trouble.
+# And well, this is free software, so there isn't a warrantee, but
+# I disclaim all ability to help you if you do enable this without
+# modifying IsRTAddress below.
+
+$ParseNewMessageForTicketCcs = undef;
+
+# IsRTAddress is used to make sure RT doesn't add itself as a ticket CC if
+# the setting above is enabled.
+
+sub IsRTAddress {
+    my $address = shift;
+
+    # Example: the following rule would tell RT not to Cc 
+    #  "tickets@noc.example.com"
+    # return(1) if ($address =~ /^tickets\@noc.example.com$/i);
+    
+    return(undef)
+}
+
+# CanonicalizeAddress converts email addresses into canonical form.
+# it takes one email address in and returns the proper canonical
+# form. You can dump whatever your proper local config is in here
+
+sub CanonicalizeAddress {
+    my $email = shift;
+    # Example: the following rule would treat all email
+    # coming from a subdomain as coming from second level domain
+    # foo.com
+    #$email =~ s/\@(.*).foo.com/\@foo.com/;
+    return ($email)
+}
+
+# If $LookupSenderInExternalDatabase is defined, RT will attempt to
+# verify the incoming message sender with a known source, using the 
+# LookupExternalUserInfo routine below
+
+$LookupSenderInExternalDatabase = undef;
+
+# If $SenderMustExistInExternalDatabase is true, RT will refuse to
+# create non-privileged accounts for unknown users if you are using 
+# the "LookupSenderInExternalDatabase" option.
+# Instead, an error message will be mailed and RT will forward the 
+# message to $RTOwner.
+#
+# If you are not using $LookupSenderInExternalDatabase, this option
+# has no effect.
+#
+# If you define an AutoRejectRequest template, RT will use this   
+# template for the rejection message.
+
+$SenderMustExistInExternalDatabase = undef;
+
+# LookupExternalUserInfo is a site-definable method for synchronizing
+# incoming users with an external data source. 
+#
+# This routine takes a tuple of EmailAddress and FriendlyName
+#      EmailAddress is the user's email address, ususally taken from
+#              an email message's From: header.
+#      FriendlyName is a freeform string, ususally taken from the "comment" 
+#              portion of an email message's From: header.
+#
+# It returns (FoundInExternalDatabase, ParamHash);
+#
+#   FoundInExternalDatabase must  be set to 1 before return if the user was
+#   found in the external database.
+#
+#   ParamHash is a Perl parameter hash which can contain at least the following
+#   fields. These fields are used to populate RT's users database when the user 
+#   is created
+#
+#      EmailAddress is the email address that RT should use for this user.  
+#      Name is the 'Name' attribute RT should use for this user. 
+#           'Name' is used for things like access control and user lookups.
+#      RealName is what RT should display as the user's name when displaying 
+#           'friendly' names
+
+sub LookupExternalUserInfo {
+  my ($EmailAddress, $RealName) = @_;
+
+  my $FoundInExternalDatabase = 1;
+  my %params = {};
+  
+  #Name is the RT username you want to use for this user.
+  $params{'Name'} = $EmailAddress;
+  $params{'EmailAddress'} = $EmailAddress;
+  $params{'RealName'} = $RealName;
+
+  # See RT's contributed code for examples.
+  # http://www.fsck.com/pub/rt/contrib/
+  return ($FoundInExternalDatabase, %params); 
+}
+
+# }}}
+
+# {{{ Outgoing mail configuration
+
+# RT is designed such that any mail which already has a ticket-id associated
+# with it will get to the right place automatically.
+
+# $CorrespondAddress and $CommentAddress are the default addresses 
+# that will be listed in From: and Reply-To: headers of correspondence
+# and comment mail tracked by RT, unless overridden by a queue-specific
+# address. 
+
+$CorrespondAddress='RT::CorrespondAddress.not.set';
+
+$CommentAddress='RT::CommentAddress.not.set';
+
+
+#Sendmail Configuration
+
+# $MailCommand defines which method RT will use to try to send mail
+# We know that 'sendmailpipe' works fairly well.
+# If 'sendmailpipe' doesn't work well for you, try 'sendmail' 
+#
+# Note that you should remove the '-t' from $SendmailArguments 
+# if you use 'sendmail rather than 'sendmailpipe'
+
+$MailCommand = 'sendmailpipe';
+
+# $SendmailArguments defines what flags to pass to $Sendmail
+# assuming you picked 'sendmail' or 'sendmailpipe' as the $MailCommand above.
+# If you picked 'sendmailpipe', you MUST add a -t flag to $SendmailArguments
+
+# These options are good for most sendmail wrappers and workalikes
+$SendmailArguments="-oi -t";
+
+# These arguments are good for sendmail brand sendmail 8 and newer
+#$SendmailArguments="-oi -t -ODeliveryMode=b -OErrorMode=m";
+
+# If you selected 'sendmailpipe' above, you MUST specify the path
+# to your sendmail binary in $SendmailPath.  
+# !! If you did not # select 'sendmailpipe' above, this has no effect!!
+$SendmailPath = "/usr/sbin/sendmail";
+
+# RT can optionally set a "Friendly" 'To:' header when sending messages to 
+# Ccs or AdminCcs (rather than having a blank 'To:' header.
+
+# This feature DOES NOT WORK WITH SENDMAIL[tm] BRAND SENDMAIL
+# If you are using sendmail, rather than postfix, qmail, exim or some other MTA,
+# you _must_ disable this option.
+
+$UseFriendlyToLine = 0;
+
+
+# }}}
+
+# {{{ Logging
+
+# Logging.  The default is to log anything except debugging
+# information to a logfile.  Check the Log::Dispatch POD for
+# information about how to get things by syslog, mail or anything
+# else, get debugging info in the log, etc. 
+
+#  It might generally make
+# sense to send error and higher by email to some administrator. 
+# If you do this, be careful that this email isn't sent to this RT instance.
+
+
+# the minimum level error that will be logged to the specific device.
+# levels from lowest to highest:  
+#  debug info notice warning error critical alert emergency 
+
+
+#  Mail loops will generate a critical log message.
+
+$LogToScreen = 'error';
+$LogToFile = 'error';
+#$LogToFileNamed = "$LogDir/rt.log.".$$.".".$<; #log to rt.log.<pid>.<user>
+$LogToFileNamed = "$LogDir/rt.log".$<; #log to rt.log.user;
+
+# }}}
+
+# {{{ Web interface configuration
+
+
+
+# Define the directory name to be used for images in rt web
+# documents.
+
+# If you're putting the web ui somewhere other than at the root of
+# your server
+# $WebPath requires a leading / but no trailing /     
+
+$WebPath = "";
+
+# This is the Scheme, server and port for constructing urls to webrt
+# $WebBaseURL doesn't need a trailing /                                                                            
+
+$WebBaseURL = "http://RT::WebBaseURL.not.configured:80";
+
+$WebURL = $WebBaseURL . $WebPath . "/";
+
+
+
+# $WebImagesURL points to the base URL where RT can find its images.
+# If you're running the FastCGI version of the RT web interface,
+# you should make RT's WebRT/html/NoAuth/images directory available on 
+# a static web server and supply that URL as $WebImagesURL.
+
+$WebImagesURL = $WebURL."NoAuth/images/";
+
+# $RTLogoURL points to the URL of the RT logo displayed in the web UI
+
+$LogoURL = $WebImagesURL."rt.jpg";
+
+# If $WebExternalAuth is defined, RT will defer to the environment's
+# REMOTE_USER variable.
+
+$WebExternalAuth = undef;
+
+# $MasonComponentRoot is where your rt instance keeps its mason html files
+# (this should be autoconfigured during 'make install' or 'make upgrade')
+
+$MasonComponentRoot = "!!MASON_HTML_PATH!!";
+
+# $MasonLocalComponentRoot is where your rt instance keeps its site-local
+# mason html files.
+# (this should be autoconfigured during 'make install' or 'make upgrade')
+
+$MasonLocalComponentRoot = "!!MASON_LOCAL_HTML_PATH!!";
+
+# $MasonDataDir Where mason keeps its datafiles
+# (this should be autoconfigured during 'make install' or 'make upgrade')
+
+$MasonDataDir = "!!MASON_DATA_PATH!!";
+
+# RT needs to put session data (for preserving state between connections
+# via the web interface)
+$MasonSessionDir = "!!MASON_SESSION_PATH!!";
+
+
+
+#This is from tobias' prototype web search UI. it may stay and it may go.
+%WebOptions=
+    (
+     # This is for putting in more user-actions at the Transaction
+     # bar.  I will typically add "Enter bug in Bugzilla" here.:
+     ExtraTransactionActions => sub { return ""; },
+
+     # Here you can modify the list view.  Be aware that the web
+     # interface might crash if TicketAttribute is wrongly set.
+     
+     QueueListingCols => 
+      [
+       { Header     => 'Id',
+        TicketLink => 1,
+        TicketAttribute => 'Id'
+        },
+
+      { Header     => 'Subject',
+        TicketAttribute => 'Subject'
+        },
+       { Header => 'Requestor(s)',
+        TicketAttribute => 'RequestorsAsString'
+        },
+       { Header => 'Status',
+        TicketAttribute => 'Status'
+        },
+
+
+       { Header => 'Queue',
+        TicketAttribute => 'QueueObj->Name'
+        },
+
+
+
+       { Header => 'Told',
+        TicketAttribute => 'ToldObj->AgeAsString'
+        },
+
+       { Header => 'Age',
+        TicketAttribute => 'CreatedObj->AgeAsString'
+        },
+
+       { Header => 'Last',
+        TicketAttribute => 'LastUpdatedObj->AgeAsString'
+        },
+
+       # TODO: It would be nice with a link here to the Owner and all
+       # other request owned by this Owner.
+       { Header => 'Owner',
+        TicketAttribute => 'OwnerObj->Name'
+       },
+   
+       { Header     => 'Take',
+        TicketLink => 1,
+        Constant   => 'Take',
+        ExtraLinks => '&Action=Take'
+        },
+
+      ]
+     );
+
+# }}}
+
+# {{{ RT Linking Interface
+
+# $TicketBaseURI is the Base path of the URI for local tickets
+
+# You shouldn't need to touch this. it's used to link tickets both locally
+# and remotely
+
+$TicketBaseURI = "fsck.com-rt://$Organization/$rtname/ticket/";
+
+# A hash table of conversion subs to be used for transforming RT Link
+# URIs to URLs in the web interface.  If you want to use RT towards
+# locally installed databases, this is the right place to configure it.
+
+%URI2HTTP=
+    (
+      'http' => sub {return @_;},
+      'https' => sub {return @_;},
+      'ftp' => sub {return @_;},
+     'fsck.com-rt' => sub {warn "stub!";},
+     'mozilla.org-bugzilla' => sub {warn "stub!"},
+     'fsck.com-kb' => sub {warn "stub!"}
+     );
+
+
+# A hash table of subs for fetching content from an URI
+%ContentFromURI=   
+    (
+     'fsck.com-rt' => sub {warn "stub!";},
+     'mozilla.org-bugzilla' => sub {warn "stub!"},
+     'fsck.com-kb' => sub {warn "stub!"}
+     );
+
+# }}}
+
+# {{{ No User servicable parts inside 
+
+############################################
+############################################
+############################################
+#
+#  Don't edit anything below this line unless you really know
+#  what you're doing
+#
+#
+############################################
+############################################
+
+# TODO: get this stuff out of the config file and into RT.pm
+
+#Set up us the timezone
+$ENV{'TZ'} = $Timezone; #TODO: Bogus hack to deal with Date::Manip whining
+
+# Configure sendmail if we're using Entity->send('sendmail')
+if ($MailCommand eq 'sendmail') {
+    $MailParams = $SendmailArguments;
+}
+
+
+
+# }}}
+
+
+1;
diff --git a/rt/etc/rt.spec b/rt/etc/rt.spec
new file mode 100644 (file)
index 0000000..14200c1
--- /dev/null
@@ -0,0 +1,137 @@
+Summary: rt Request Tracker
+
+Name: rt
+Version: 2.0.9pre5
+Release: 1
+Group: Applications/Web
+Packager: Jesse Vincent <jesse@bestpractical.com>
+Vendor: http://www.fsck.com/projects/rt
+Requires: perl
+Requires: mod_perl > 1.22
+Requires: perl-DBI >= 1.18
+Requires: perl-DBIx-DataSource >= 0.02
+Requires: perl-DBIx-SearchBuilder >= 0.47
+Requires: perl-HTML-Parser
+Requires: perl-MLDBM
+Requires: perl-libnet
+Requires: perl-CGI.pm >= 2.78
+Requires: perl-Params-Validate >= 0.02
+Requires: perl-HTML-Mason >= 0.896
+Requires: perl-libapreq
+Requires: perl-Apache-Session >= 1.53
+Requires: perl-MIME-tools >= 5.411
+Requires: perl-MailTools >= 1.20
+Requires: perl-Getopt-Long >= 2.24
+Requires: perl-Tie-IxHash
+Requires: perl-TimeDate
+Requires: perl-Time-HiRes
+Requires: perl-Text-Wrapper
+Requires: perl-Text-Template
+Requires: perl-File-Spec >= 0.8
+Requires: perl-FreezeThaw
+Requires: perl-Storable
+Requires: perl-File-Temp
+Requires: perl-Log-Dispatch >= 1.6                     
+
+Source: http://www.fsck.com/pub/rt/release/%{name}.tar.gz
+Copyright: GPL 
+BuildRoot: /var/tmp/rt-root
+
+%description
+RT is an industrial-grade ticketing system. It lets a group
+of people intelligently and efficiently manage requests
+submitted by a community of users. RT is used by systems
+administrators, customer support staffs, NOCs, developers
+and even marketing departments at over a thousand sites
+around the world. 
+
+%prep
+groupadd rt || true
+%setup -q -n %{name}
+
+%build
+
+%install
+
+if [ x$RPM_BUILD_ROOT != x ]; then
+rm -rf $RPM_BUILD_ROOT
+fi
+
+#
+# Perform all the non-site specfic steps whilst building the package
+#
+make dirs libs-install html-install bin-install  DESTDIR=$RPM_BUILD_ROOT
+#
+# fixperms needs these, so make fake empty files
+touch $RPM_BUILD_ROOT/opt/rt2/etc/insertdata $RPM_BUILD_ROOT/opt/rt2/etc/config.pm
+make fixperms insert-install WEB_USER=www DESTDIR=$RPM_BUILD_ROOT
+
+#
+# Copy in the files needed again after install
+#
+mkdir -p $RPM_BUILD_ROOT/opt/rt2/postinstall/bin
+cp -rp Makefile etc tools $RPM_BUILD_ROOT/opt/rt2/postinstall
+cp -rp bin/initacls.* $RPM_BUILD_ROOT/opt/rt2/postinstall/bin
+
+# logging in /var/log/rt2
+mkdir -p $RPM_BUILD_ROOT/var/log/rt2
+chown www $RPM_BUILD_ROOT/var/log/rt2
+chgrp rt $RPM_BUILD_ROOT/var/log/rt2
+chmod ug=rwx,o= $RPM_BUILD_ROOT/var/log/rt2
+
+%clean
+if [ x$RPM_BUILD_ROOT != x ]; then
+rm -rf $RPM_BUILD_ROOT
+fi
+
+#
+# A new rt groups is required
+#
+%pre
+groupadd rt || true
+
+#
+# Show the user the site specific steps required after install
+#
+%post
+cat <<EOF
+-----------------------------------------------------------------------
+rt2 installation is complete. Now create the rt2 database by running:
+-----------------------------------------------------------------------
+
+# cd /opt/rt2/postinstall
+# make config-replace initialize.mysql insert RT_LOG_PATH=/var/log/rt2 DB_RT_PASS=new_rt_user_password
+
+Choose your own new_rt_user_password. You will need the mysql root password.
+You can try Pg or Oracle instead of mysql - untested.
+
+Review and configure your site specific details in /opt/rt2/etc/config.pm
+EOF
+
+%preun
+
+%files
+%dir /opt/rt2
+/opt/rt2/bin
+/opt/rt2/WebRT
+/opt/rt2/lib
+/opt/rt2/local
+/opt/rt2/man
+/opt/rt2/postinstall
+%dir /opt/rt2/etc
+/opt/rt2/etc/insertdata
+%config /opt/rt2/etc/config.pm
+%dir /var/log/rt2
+
+%changelog
+* Mon Sep 24 2001 Jesse Vincent <jesse@bestpractical.com>
+  Switch to rt DESTDIR support
+* Fri Sep 14 2001 Cris Bailiff <c.bailiff@devsecure.com>
+  Fix permissions on created /var/log/rt2 and roll in 2.0.7
+* Tue Sep 4 2001 Cris Bailiff <c.bailiff@devsecure.com>
+- created initial spec file
+* Tue Sep 4 2001 Cris Bailiff <c.bailiff@devsecure.com>
+- created initial spec file
+* Tue Sep 4 2001 Cris Bailiff <c.bailiff@devsecure.com>
+- created initial spec file
diff --git a/rt/etc/schema.Oracle b/rt/etc/schema.Oracle
new file mode 100644 (file)
index 0000000..0c14cb3
--- /dev/null
@@ -0,0 +1,287 @@
+CREATE SEQUENCE KEYWORDSELECTS_seq;
+CREATE TABLE KeywordSelects (
+       id              NUMBER(11, 0) PRIMARY KEY,
+       Name            VARCHAR2(255),
+       Keyword         NUMBER(11, 0),
+       Single          NUMBER(11, 0),
+       Depth           NUMBER(11, 0) DEFAULT 0,
+       ObjectType      VARCHAR2(32) NOT NULL,
+       ObjectField     VARCHAR2(32),
+       ObjectValue     VARCHAR2(255),
+       Disabled                NUMBER(11, 0) DEFAULT 0
+);
+
+CREATE INDEX KeywordSelects1 ON KeywordSelects (Keyword);
+CREATE INDEX KeywordSelects2 ON 
+       KeywordSelects(ObjectType, ObjectField, ObjectValue);
+
+
+CREATE SEQUENCE ATTACHMENTS_seq;
+CREATE TABLE Attachments (
+       id              NUMBER(11,0) PRIMARY KEY,
+       TransactionId   NUMBER(11,0) NOT NULL,
+       Parent          NUMBER(11,0),           
+       MessageId       VARCHAR2(160),
+       Subject         VARCHAR2(255),
+       Filename        VARCHAR2(255),
+       ContentType     VARCHAR2(80),
+       ContentEncoding         VARCHAR2(80),
+       Content         CLOB,
+       Headers         CLOB,
+       Creator         NUMBER(11,0),
+       Created         DATE,
+       Disabled        NUMBER(11,0) DEFAULT 0
+);
+
+CREATE SEQUENCE QUEUES_seq;
+CREATE TABLE Queues (
+       id                      NUMBER(11, 0) PRIMARY KEY,
+       Name                    VARCHAR2(40) NOT NULL UNIQUE,
+       Description             VARCHAR2(120),
+       CorrespondAddress       VARCHAR2(40),
+       CommentAddress          VARCHAR2(40),
+       InitialPriority         NUMBER(11, 0),          
+       FinalPriority           NUMBER(11, 0),
+       DefaultDueIn            NUMBER(11, 0),
+       Creator                 NUMBER(11, 0),
+       Created                 DATE,
+       LastUpdatedBy           NUMBER(11, 0),
+       LastUpdated             DATE,
+       Disabled                NUMBER(11,0) DEFAULT 0
+);
+
+CREATE SEQUENCE LINKS_seq;
+CREATE TABLE Links (
+       id              NUMBER(11,0) PRIMARY KEY,
+       Base            VARCHAR2(255),
+       Target          VARCHAR2(255),
+       Type            VARCHAR2(20) NOT NULL,
+       LocalTarget     NUMBER(11,0),
+       LocalBase       NUMBER(11,0),
+       LastUpdatedBy   NUMBER(11,0),
+       LastUpdated     DATE,
+       Creator         NUMBER(11,0),
+       Created         DATE
+);
+
+CREATE UNIQUE INDEX Links1 ON Links (Base, Target, Type);
+
+
+
+CREATE SEQUENCE GROUPS_seq;
+CREATE TABLE Groups (
+       id              NUMBER(11,0) PRIMARY KEY,
+       Name            VARCHAR2(16) UNIQUE,
+       Description     VARCHAR(64),
+       Pseudo          NUMBER(11,0) DEFAULT 0
+);
+
+CREATE SEQUENCE WATCHERS_seq;
+CREATE TABLE Watchers (
+       id              NUMBER(11,0) PRIMARY KEY,
+       Type            VARCHAR2(16),   
+       Scope           VARCHAR2(16),   
+       Value           NUMBER(11,0),   
+       Email           VARCHAR2(255),  
+       Quiet           NUMBER(11,0),   
+       Owner           NUMBER(11,0),   
+       Creator         NUMBER(11,0),
+       Created         DATE,
+       LastUpdatedBy   NUMBER(11,0),
+       LastUpdated     DATE
+);
+
+
+
+CREATE SEQUENCE SCRIPCONDITIONS_seq;
+CREATE TABLE ScripConditions (
+       id                      NUMBER(11, 0) PRIMARY KEY,
+       Name                    VARCHAR2(255),
+       Description             VARCHAR2(255),
+       ExecModule              VARCHAR2(60),
+       Argument                VARCHAR2(255),
+       ApplicableTransTypes    VARCHAR2(60),
+       Creator                 NUMBER(11, 0),
+       Created                 DATE,
+       LastUpdatedBy           NUMBER(11, 0),
+       LastUpdated             DATE
+);
+
+
+CREATE SEQUENCE TRANSACTIONS_seq;
+CREATE TABLE Transactions (
+       id                      NUMBER(11,0) PRIMARY KEY,
+       EffectiveTicket         NUMBER(11,0),
+       Ticket                  NUMBER(11,0),
+       TimeTaken               NUMBER(11,0),
+       Type                    VARCHAR2(20),
+       Field                   VARCHAR2(40),
+       OldValue                VARCHAR2(255),
+       NewValue                VARCHAR2(255),
+       Data                    VARCHAR2(100),
+       Creator                 NUMBER(11,0),
+       Created                 DATE,
+       Disabled                NUMBER(11,0) DEFAULT 0
+);
+
+CREATE SEQUENCE SCRIPS_seq;
+CREATE TABLE Scrips (
+       id              NUMBER(11,0) PRIMARY KEY,       
+       ScripCondition  NUMBER(11,0),
+       ScripAction     NUMBER(11,0),
+       Stage           VARCHAR2(32),
+       Queue           NUMBER(11,0),
+       Template        NUMBER(11,0),
+       Creator         NUMBER(11,0),
+       Created         DATE,
+       LastUpdatedBy   NUMBER(11,0),
+       LastUpdated     DATE  
+);
+
+
+
+
+CREATE SEQUENCE ACL_seq;
+CREATE TABLE ACL (
+       id              NUMBER(11,0) PRIMARY KEY,
+       PrincipalId     NUMBER(11,0),
+       PrincipalType   VARCHAR2(25),
+       RightName       VARCHAR2(25),
+       RightScope      VARCHAR2(25),
+       RightAppliesTo  NUMBER(11,0)
+);
+
+CREATE SEQUENCE GROUPMEMBERS_seq;
+CREATE TABLE GroupMembers (
+       id              NUMBER(11,0) PRIMARY KEY,
+       GroupId         NUMBER(11,0),
+       UserId          NUMBER(11,0) 
+);
+
+CREATE UNIQUE INDEX GroupMembers1 ON GroupMembers (GroupId, UserId);
+
+
+CREATE SEQUENCE OBJECTKEYWORDS_seq;
+CREATE TABLE ObjectKeywords (
+  id           NUMBER(11,0)  PRIMARY KEY,
+  Keyword      NUMBER(11,0) NOT NULL,
+  KeywordSelect NUMBER(11,0)  NOT NULL,
+  ObjectType   VARCHAR2(32) NOT NULL,
+  ObjectId     NUMBER(11,0) NOT NULL
+);
+
+CREATE UNIQUE INDEX ObjectKeywords1 ON ObjectKeywords
+       (ObjectId, ObjectType, KeywordSelect, Keyword);
+CREATE INDEX ObjectKeywords3 ON ObjectKeywords (Keyword);
+
+CREATE SEQUENCE KEYWORDS_seq;
+CREATE TABLE Keywords (
+       id              NUMBER(11, 0) PRIMARY KEY,
+       Name            VARCHAR2(255) NOT NULL,
+       Description     VARCHAR2(255),
+       Parent          NUMBER(11, 0),
+       Disabled                NUMBER(11, 0) DEFAULT 0
+);
+
+CREATE UNIQUE INDEX Keywords1 ON Keywords (Name, Parent);
+CREATE INDEX Keywords3 ON Keywords (Parent);
+
+CREATE SEQUENCE USERS_seq;
+CREATE TABLE Users (
+       id                      NUMBER(11,0) PRIMARY KEY,
+       Name                    VARCHAR2(120) NOT NULL UNIQUE,
+       Password                VARCHAR2(40),
+       Comments                CLOB,
+       Signature               CLOB,
+       EmailAddress            VARCHAR2(120),
+       FreeFormContactInfo     CLOB,
+       Organization            VARCHAR2(200),
+       Privileged              NUMBER(11,0),
+       RealName                VARCHAR2(120),
+       NickName                VARCHAR2(16),
+       Lang                    VARCHAR2(16),
+       EmailEncoding           VARCHAR2(16),
+       WebEncoding             VARCHAR2(16),
+       ExternalContactInfoId   VARCHAR2(100),
+       ContactInfoSystem       VARCHAR2(30),
+       ExternalAuthId          VARCHAR2(100),
+       AuthSystem              VARCHAR2(30),
+       Gecos                   VARCHAR2(16),
+       HomePhone               VARCHAR2(30),
+       WorkPhone               VARCHAR2(30),
+       MobilePhone             VARCHAR2(30),
+       PagerPhone              VARCHAR2(30),
+       Address1                VARCHAR2(200),
+       Address2                VARCHAR2(200),
+       City                    VARCHAR2(100),
+       State                   VARCHAR2(100),
+       Zip                     VARCHAR2(16),
+       Country                 VARCHAR2(50),
+       Creator                 NUMBER(11,0),
+       Created                 DATE,
+       LastUpdatedBy           NUMBER(11,0),
+       LastUpdated             DATE,
+       Disabled                        NUMBER(11,0) DEFAULT 0
+);
+
+
+
+
+CREATE SEQUENCE TICKETS_seq;
+CREATE TABLE Tickets (
+       id                      NUMBER(11, 0) PRIMARY KEY,
+       EffectiveId             NUMBER(11, 0),
+       Queue                   NUMBER(11,0),
+       Type                    VARCHAR2(16),           
+       IssueStatement          NUMBER(11,0),   
+       Resolution              NUMBER(11,0),           
+       Owner                   NUMBER(11,0),           
+       Subject                 VARCHAR2(200) DEFAULT '', 
+       InitialPriority         NUMBER(11,0) DEFAULT 0,
+       FinalPriority           NUMBER(11,0) DEFAULT 0,
+       Priority                NUMBER(11,0) DEFAULT 0,
+       Status                  VARCHAR2(10),           
+       TimeWorked              NUMBER(11,0) DEFAULT 0,
+       TimeLeft                NUMBER(11,0) DEFAULT 0,
+       Told                    DATE,
+       Starts                  DATE,
+       Started                 DATE,
+       Due                     DATE,
+       Resolved                DATE,
+       LastUpdatedBy           NUMBER(11,0),
+       LastUpdated             DATE,
+       Creator                 NUMBER(11,0),
+       Created                 DATE,
+       Disabled                NUMBER(11,0) DEFAULT 0
+);
+
+CREATE SEQUENCE SCRIPACTIONS_seq;
+CREATE TABLE ScripActions (
+  id           NUMBER(11,0) PRIMARY KEY,
+  Name         VARCHAR2(255),
+  Description  VARCHAR2(255),
+  ExecModule   VARCHAR2(60),
+  Argument     VARCHAR2(255),
+  Creator      NUMBER(11,0),
+  Created      DATE,
+  LastUpdatedBy        NUMBER(11,0),
+  LastUpdated  DATE
+);
+
+
+CREATE SEQUENCE TEMPLATES_seq;
+CREATE TABLE Templates (
+       id              NUMBER(11,0) PRIMARY KEY,
+       Queue           NUMBER(11,0) DEFAULT 0 NOT NULL,
+       Name            VARCHAR2(40) NOT NULL UNIQUE,
+       Description     VARCHAR2(120),
+       Type            VARCHAR2(16),
+       Language        VARCHAR2(16), 
+       TranslationOf   NUMBER(11,0),
+       Content         CLOB,
+       LastUpdated     DATE,
+       LastUpdatedBy   NUMBER(11,0),
+       Creator         NUMBER(11,0),
+       Created         DATE
+);
+
diff --git a/rt/etc/schema.Pg b/rt/etc/schema.Pg
new file mode 100755 (executable)
index 0000000..21d981b
--- /dev/null
@@ -0,0 +1,267 @@
+CREATE TABLE KeywordSelects (
+  id serial NOT NULL  ,
+  Name varchar(255)   ,
+  Keyword integer   ,
+  Single integer   ,
+  Depth integer NOT NULL DEFAULT 0 ,
+  ObjectType varchar(32) NOT NULL  ,
+  ObjectField varchar(32)   ,
+  ObjectValue varchar(255)   ,
+  Disabled int2 NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX KeywordSelects1 ON KeywordSelects (Keyword);
+CREATE INDEX KeywordSelects2 ON KeywordSelects (ObjectType, ObjectField, ObjectValue);
+CREATE TABLE Attachments (
+  id serial NOT NULL  ,
+  TransactionId integer NOT NULL  ,
+  Parent integer   ,
+  MessageId varchar(160)   ,
+  Subject varchar(255)   ,
+  Filename varchar(255)   ,
+  ContentType varchar(80)   ,
+  ContentEncoding varchar(80)   ,
+  Content TEXT   ,
+  Headers TEXT   ,
+  Creator integer   ,
+  Created timestamp   ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX Attachments1 ON Attachments (Parent);
+CREATE INDEX Attachments2 ON Attachments (TransactionId);
+CREATE INDEX Attachments3 ON Attachments (Parent, TransactionId);
+CREATE TABLE Queues (
+  id serial NOT NULL  ,
+  Name varchar(120) NOT NULL  ,
+  Description varchar(120)   ,
+  CorrespondAddress varchar(120)   ,
+  CommentAddress varchar(120)   ,
+  InitialPriority integer   ,
+  FinalPriority integer   ,
+  DefaultDueIn integer   ,
+  Creator integer   ,
+  Created timestamp   ,
+  LastUpdatedBy integer   ,
+  LastUpdated timestamp   ,
+  Disabled int2 NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX Queues1 ON Queues (Name);
+CREATE TABLE Links (
+  id serial NOT NULL  ,
+  Base varchar(240)   ,
+  Target varchar(240)   ,
+  Type varchar(20) NOT NULL  ,
+  LocalTarget integer   ,
+  LocalBase integer   ,
+  LastUpdatedBy integer   ,
+  LastUpdated timestamp   ,
+  Creator integer   ,
+  Created timestamp   ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX Links1 ON Links (Base, Target, Type);
+CREATE TABLE Groups (
+  id serial NOT NULL  ,
+  Name varchar(16)   ,
+  Description varchar(64)   ,
+  Pseudo integer NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX Groups1 ON Groups (Name);
+CREATE TABLE Watchers (
+  id serial NOT NULL  ,
+  Type varchar(16)   ,
+  Scope varchar(16)   ,
+  Value integer   ,
+  Email varchar(255)   ,
+  Quiet integer   ,
+  Owner integer   ,
+  Creator integer   ,
+  Created timestamp   ,
+  LastUpdatedBy integer   ,
+  LastUpdated timestamp   ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX Watchers1 ON Watchers (Scope, Value, Type, Owner);
+CREATE TABLE ScripConditions (
+  id serial NOT NULL  ,
+  Name varchar(255)   ,
+  Description varchar(255)   ,
+  ExecModule varchar(60)   ,
+  Argument varchar(255)   ,
+  ApplicableTransTypes varchar(60)   ,
+  Creator integer   ,
+  Created timestamp   ,
+  LastUpdatedBy integer   ,
+  LastUpdated timestamp   ,
+  PRIMARY KEY (id)
+);
+CREATE TABLE Transactions (
+  id serial NOT NULL  ,
+  EffectiveTicket integer   ,
+  Ticket integer   ,
+  TimeTaken integer   ,
+  Type varchar(20)   ,
+  Field varchar(40)   ,
+  OldValue varchar(255)   ,
+  NewValue varchar(255)   ,
+  Data varchar(100)   ,
+  Creator integer   ,
+  Created timestamp   ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX Transactions1 ON Transactions (Ticket);
+CREATE INDEX Transactions2 ON Transactions (EffectiveTicket);
+CREATE TABLE Scrips (
+  id serial NOT NULL  ,
+  ScripCondition integer   ,
+  ScripAction integer   ,
+  Stage varchar(32)   ,
+  Queue integer   ,
+  Template integer   ,
+  Creator integer   ,
+  Created timestamp   ,
+  LastUpdatedBy integer   ,
+  LastUpdated timestamp   ,
+  PRIMARY KEY (id)
+);
+CREATE TABLE ACL (
+  id serial NOT NULL  ,
+  PrincipalId integer   ,
+  PrincipalType varchar(25)   ,
+  RightName varchar(25)   ,
+  RightScope varchar(25)   ,
+  RightAppliesTo integer   ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX ACL1 ON ACL (RightScope, PrincipalId);
+CREATE INDEX ACL2 ON ACL (RightScope, RightAppliesTo, RightName, PrincipalType, PrincipalId);
+CREATE TABLE GroupMembers (
+  id serial NOT NULL  ,
+  GroupId integer   ,
+  UserId integer   ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX GroupMembers1 ON GroupMembers (GroupId, UserId);
+CREATE TABLE ObjectKeywords (
+  id serial NOT NULL  ,
+  Keyword integer NOT NULL  ,
+  KeywordSelect integer NOT NULL  ,
+  ObjectType varchar(32) NOT NULL  ,
+  ObjectId integer NOT NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX ObjectKeywords1 ON ObjectKeywords (ObjectId, ObjectType, KeywordSelect, Keyword);
+CREATE INDEX ObjectKeywords2 ON ObjectKeywords (ObjectId, ObjectType);
+CREATE INDEX ObjectKeywords3 ON ObjectKeywords (Keyword);
+CREATE TABLE Keywords (
+  id serial NOT NULL  ,
+  Name varchar(255) NOT NULL  ,
+  Description varchar(255)   ,
+  Parent integer   ,
+  Disabled int2 NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX Keywords1 ON Keywords (Name, Parent);
+CREATE INDEX Keywords2 ON Keywords (Name);
+CREATE INDEX Keywords3 ON Keywords (Parent);
+CREATE TABLE Users (
+  id serial NOT NULL  ,
+  Name varchar(120) NOT NULL  ,
+  Password varchar(40)   ,
+  Comments TEXT   ,
+  Signature TEXT   ,
+  EmailAddress varchar(120)   ,
+  FreeformContactInfo TEXT   ,
+  Organization varchar(200)   ,
+  Privileged integer   ,
+  RealName varchar(120)   ,
+  Nickname varchar(16)   ,
+  Lang varchar(16)   ,
+  EmailEncoding varchar(16)   ,
+  WebEncoding varchar(16)   ,
+  ExternalContactInfoId varchar(100)   ,
+  ContactInfoSystem varchar(30)   ,
+  ExternalAuthId varchar(100)   ,
+  AuthSystem varchar(30)   ,
+  Gecos varchar(16)   ,
+  HomePhone varchar(30)   ,
+  WorkPhone varchar(30)   ,
+  MobilePhone varchar(30)   ,
+  PagerPhone varchar(30)   ,
+  Address1 varchar(200)   ,
+  Address2 varchar(200)   ,
+  City varchar(100)   ,
+  State varchar(100)   ,
+  Zip varchar(16)   ,
+  Country varchar(50)   ,
+  Creator integer   ,
+  Created timestamp   ,
+  LastUpdatedBy integer   ,
+  LastUpdated timestamp   ,
+  Disabled int2 NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX Users1 ON Users (Name);
+CREATE INDEX Users3 ON Users (id, EmailAddress);
+CREATE INDEX Users4 ON Users (EmailAddress);
+CREATE TABLE Tickets (
+  id serial NOT NULL  ,
+  EffectiveId integer   ,
+  Queue integer   ,
+  Type varchar(16)   ,
+  IssueStatement integer   ,
+  Resolution integer   ,
+  Owner integer   ,
+  Subject varchar(200)  DEFAULT '[no subject]' ,
+  InitialPriority integer   ,
+  FinalPriority integer   ,
+  Priority integer   ,
+  Status varchar(10)   ,
+  TimeWorked integer   ,
+  TimeLeft integer   ,
+  Told timestamp   ,
+  Starts timestamp   ,
+  Started timestamp   ,
+  Due timestamp   ,
+  Resolved timestamp   ,
+  LastUpdatedBy integer   ,
+  LastUpdated timestamp   ,
+  Creator integer   ,
+  Created timestamp   ,
+  Disabled int2 NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX Tickets1 ON Tickets (Queue, Status);
+CREATE INDEX Tickets2 ON Tickets (Owner);
+CREATE INDEX Tickets3 ON Tickets (EffectiveId);
+CREATE INDEX Tickets4 ON Tickets (id, Status);
+CREATE INDEX Tickets5 ON Tickets (id, EffectiveId);
+CREATE TABLE ScripActions (
+  id serial NOT NULL  ,
+  Name varchar(255)   ,
+  Description varchar(255)   ,
+  ExecModule varchar(60)   ,
+  Argument varchar(255)   ,
+  Creator integer   ,
+  Created timestamp   ,
+  LastUpdatedBy integer   ,
+  LastUpdated timestamp   ,
+  PRIMARY KEY (id)
+);
+CREATE TABLE Templates (
+  id serial NOT NULL  ,
+  Queue integer NOT NULL DEFAULT 0 ,
+  Name varchar(40) NOT NULL  ,
+  Description varchar(120)   ,
+  Type varchar(16)   ,
+  Language varchar(16)   ,
+  TranslationOf integer   ,
+  Content TEXT   ,
+  LastUpdated timestamp   ,
+  LastUpdatedBy integer   ,
+  Creator integer   ,
+  Created timestamp   ,
+  PRIMARY KEY (id)
+);
diff --git a/rt/etc/schema.mysql b/rt/etc/schema.mysql
new file mode 100755 (executable)
index 0000000..7e715c2
--- /dev/null
@@ -0,0 +1,267 @@
+CREATE TABLE KeywordSelects (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Name varchar(255) NULL  ,
+  Keyword integer NULL  ,
+  Single integer NULL  ,
+  Depth integer NOT NULL DEFAULT 0 ,
+  ObjectType varchar(32) NOT NULL  ,
+  ObjectField varchar(32) NULL  ,
+  ObjectValue varchar(255) NULL  ,
+  Disabled int2 NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX KeywordSelects1 ON KeywordSelects (Keyword);
+CREATE INDEX KeywordSelects2 ON KeywordSelects (ObjectType, ObjectField, ObjectValue);
+CREATE TABLE Attachments (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  TransactionId integer NOT NULL  ,
+  Parent integer NULL  ,
+  MessageId varchar(160) NULL  ,
+  Subject varchar(255) NULL  ,
+  Filename varchar(255) NULL  ,
+  ContentType varchar(80) NULL  ,
+  ContentEncoding varchar(80) NULL  ,
+  Content LONGTEXT NULL  ,
+  Headers LONGTEXT NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX Attachments1 ON Attachments (Parent);
+CREATE INDEX Attachments2 ON Attachments (TransactionId);
+CREATE INDEX Attachments3 ON Attachments (Parent, TransactionId);
+CREATE TABLE Queues (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Name varchar(120) NOT NULL  ,
+  Description varchar(120) NULL  ,
+  CorrespondAddress varchar(120) NULL  ,
+  CommentAddress varchar(120) NULL  ,
+  InitialPriority integer NULL  ,
+  FinalPriority integer NULL  ,
+  DefaultDueIn integer NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  LastUpdatedBy integer NULL  ,
+  LastUpdated DATETIME NULL  ,
+  Disabled int2 NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX Queues1 ON Queues (Name);
+CREATE TABLE Links (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Base varchar(240) NULL  ,
+  Target varchar(240) NULL  ,
+  Type varchar(20) NOT NULL  ,
+  LocalTarget integer NULL  ,
+  LocalBase integer NULL  ,
+  LastUpdatedBy integer NULL  ,
+  LastUpdated DATETIME NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX Links1 ON Links (Base, Target, Type);
+CREATE TABLE Groups (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Name varchar(16) NULL  ,
+  Description varchar(64) NULL  ,
+  Pseudo integer NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX Groups1 ON Groups (Name);
+CREATE TABLE Watchers (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Type varchar(16) NULL  ,
+  Scope varchar(16) NULL  ,
+  Value integer NULL  ,
+  Email varchar(255) NULL  ,
+  Quiet integer NULL  ,
+  Owner integer NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  LastUpdatedBy integer NULL  ,
+  LastUpdated DATETIME NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX Watchers1 ON Watchers (Scope, Value, Type, Owner);
+CREATE TABLE ScripConditions (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Name varchar(255) NULL  ,
+  Description varchar(255) NULL  ,
+  ExecModule varchar(60) NULL  ,
+  Argument varchar(255) NULL  ,
+  ApplicableTransTypes varchar(60) NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  LastUpdatedBy integer NULL  ,
+  LastUpdated DATETIME NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE TABLE Transactions (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  EffectiveTicket integer NULL  ,
+  Ticket integer NULL  ,
+  TimeTaken integer NULL  ,
+  Type varchar(20) NULL  ,
+  Field varchar(40) NULL  ,
+  OldValue varchar(255) NULL  ,
+  NewValue varchar(255) NULL  ,
+  Data varchar(100) NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX Transactions1 ON Transactions (Ticket);
+CREATE INDEX Transactions2 ON Transactions (EffectiveTicket);
+CREATE TABLE Scrips (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  ScripCondition integer NULL  ,
+  ScripAction integer NULL  ,
+  Stage varchar(32) NULL  ,
+  Queue integer NULL  ,
+  Template integer NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  LastUpdatedBy integer NULL  ,
+  LastUpdated DATETIME NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE TABLE ACL (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  PrincipalId integer NULL  ,
+  PrincipalType varchar(25) NULL  ,
+  RightName varchar(25) NULL  ,
+  RightScope varchar(25) NULL  ,
+  RightAppliesTo integer NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX ACL1 ON ACL (RightScope, PrincipalId);
+CREATE INDEX ACL2 ON ACL (RightScope, RightAppliesTo, RightName, PrincipalType, PrincipalId);
+CREATE TABLE GroupMembers (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  GroupId integer NULL  ,
+  UserId integer NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX GroupMembers1 ON GroupMembers (GroupId, UserId);
+CREATE TABLE ObjectKeywords (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Keyword integer NOT NULL  ,
+  KeywordSelect integer NOT NULL  ,
+  ObjectType varchar(32) NOT NULL  ,
+  ObjectId integer NOT NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX ObjectKeywords1 ON ObjectKeywords (ObjectId, ObjectType, KeywordSelect, Keyword);
+CREATE INDEX ObjectKeywords2 ON ObjectKeywords (ObjectId, ObjectType);
+CREATE INDEX ObjectKeywords3 ON ObjectKeywords (Keyword);
+CREATE TABLE Keywords (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Name varchar(255) NOT NULL  ,
+  Description varchar(255) NULL  ,
+  Parent integer NULL  ,
+  Disabled int2 NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX Keywords1 ON Keywords (Name, Parent);
+CREATE INDEX Keywords2 ON Keywords (Name);
+CREATE INDEX Keywords3 ON Keywords (Parent);
+CREATE TABLE Users (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Name varchar(120) NOT NULL  ,
+  Password varchar(40) NULL  ,
+  Comments blob NULL  ,
+  Signature blob NULL  ,
+  EmailAddress varchar(120) NULL  ,
+  FreeformContactInfo blob NULL  ,
+  Organization varchar(200) NULL  ,
+  Privileged integer NULL  ,
+  RealName varchar(120) NULL  ,
+  Nickname varchar(16) NULL  ,
+  Lang varchar(16) NULL  ,
+  EmailEncoding varchar(16) NULL  ,
+  WebEncoding varchar(16) NULL  ,
+  ExternalContactInfoId varchar(100) NULL  ,
+  ContactInfoSystem varchar(30) NULL  ,
+  ExternalAuthId varchar(100) NULL  ,
+  AuthSystem varchar(30) NULL  ,
+  Gecos varchar(16) NULL  ,
+  HomePhone varchar(30) NULL  ,
+  WorkPhone varchar(30) NULL  ,
+  MobilePhone varchar(30) NULL  ,
+  PagerPhone varchar(30) NULL  ,
+  Address1 varchar(200) NULL  ,
+  Address2 varchar(200) NULL  ,
+  City varchar(100) NULL  ,
+  State varchar(100) NULL  ,
+  Zip varchar(16) NULL  ,
+  Country varchar(50) NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  LastUpdatedBy integer NULL  ,
+  LastUpdated DATETIME NULL  ,
+  Disabled int2 NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE UNIQUE INDEX Users1 ON Users (Name);
+CREATE INDEX Users3 ON Users (id, EmailAddress);
+CREATE INDEX Users4 ON Users (EmailAddress);
+CREATE TABLE Tickets (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  EffectiveId integer NULL  ,
+  Queue integer NULL  ,
+  Type varchar(16) NULL  ,
+  IssueStatement integer NULL  ,
+  Resolution integer NULL  ,
+  Owner integer NULL  ,
+  Subject varchar(200) NULL DEFAULT '[no subject]' ,
+  InitialPriority integer NULL  ,
+  FinalPriority integer NULL  ,
+  Priority integer NULL  ,
+  Status varchar(10) NULL  ,
+  TimeWorked integer NULL  ,
+  TimeLeft integer NULL  ,
+  Told DATETIME NULL  ,
+  Starts DATETIME NULL  ,
+  Started DATETIME NULL  ,
+  Due DATETIME NULL  ,
+  Resolved DATETIME NULL  ,
+  LastUpdatedBy integer NULL  ,
+  LastUpdated DATETIME NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  Disabled int2 NOT NULL DEFAULT 0 ,
+  PRIMARY KEY (id)
+);
+CREATE INDEX Tickets1 ON Tickets (Queue, Status);
+CREATE INDEX Tickets2 ON Tickets (Owner);
+CREATE INDEX Tickets3 ON Tickets (EffectiveId);
+CREATE INDEX Tickets4 ON Tickets (id, Status);
+CREATE INDEX Tickets5 ON Tickets (id, EffectiveId);
+CREATE TABLE ScripActions (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Name varchar(255) NULL  ,
+  Description varchar(255) NULL  ,
+  ExecModule varchar(60) NULL  ,
+  Argument varchar(255) NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  LastUpdatedBy integer NULL  ,
+  LastUpdated DATETIME NULL  ,
+  PRIMARY KEY (id)
+);
+CREATE TABLE Templates (
+  id INTEGER NOT NULL  AUTO_INCREMENT,
+  Queue integer NOT NULL DEFAULT 0 ,
+  Name varchar(40) NOT NULL  ,
+  Description varchar(120) NULL  ,
+  Type varchar(16) NULL  ,
+  Language varchar(16) NULL  ,
+  TranslationOf integer NULL  ,
+  Content blob NULL  ,
+  LastUpdated DATETIME NULL  ,
+  LastUpdatedBy integer NULL  ,
+  Creator integer NULL  ,
+  Created DATETIME NULL  ,
+  PRIMARY KEY (id)
+);
diff --git a/rt/etc/schema.pm b/rt/etc/schema.pm
new file mode 100644 (file)
index 0000000..44e143e
--- /dev/null
@@ -0,0 +1,349 @@
+#   column, type, nullability, length, default, database-local
+
+my $gratuitous = {
+
+'Groups' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'Name', 'varchar', 'NULL', '16', '', '',
+    'Description', 'varchar', 'NULL', '64', '', '',
+    'Pseudo', 'integer', '', '', '0', '',
+  ],
+  'primary_key' => 'id',
+  'unique' => [ ['Name'] ],
+  'index' => [  ],
+},
+
+'ACL' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'PrincipalId', 'integer', 'NULL', '', '', '',
+    'PrincipalType', 'varchar', 'NULL', '25', '', '',
+    'RightName', 'varchar', 'NULL', '25', '', '',
+    'RightScope', 'varchar', 'NULL', '25', '', '',
+    'RightAppliesTo', 'integer', 'NULL', '', '', '',
+  ],
+  'primary_key' => 'id',
+  'unique' => [  ],
+  'index' => [ ['RightScope', 'PrincipalId'], 
+              ['RightScope','RightAppliesTo','RightName','PrincipalType','PrincipalId'] ],
+},
+
+'Watchers' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'Type', 'varchar', 'NULL', '16', '', '',
+    'Scope', 'varchar', 'NULL', '16', '', '',
+    'Value', 'integer', 'NULL', '', '', '',
+    'Email', 'varchar', 'NULL', '255', '', '',
+    'Quiet', 'integer', 'NULL', '', '', '',
+    'Owner', 'integer', 'NULL', '', '', '',
+    'Creator', 'integer', 'NULL', '', '', '',
+    'Created', 'timestamp', 'NULL', '', '', '',
+    'LastUpdatedBy', 'integer', 'NULL', '', '', '',
+    'LastUpdated', 'timestamp', 'NULL', '', '', '',
+  ],
+  'primary_key' => 'id',
+  'unique' => [  ],
+  'index' => [ ['Scope','Value','Type','Owner'] ],
+},
+
+'Links' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'Base', 'varchar', 'NULL', '240', '', '',
+    'Target', 'varchar', 'NULL', '240', '', '',
+    'Type', 'varchar', '', '20', '', '',
+    'LocalTarget', 'integer', 'NULL', '', '', '',
+    'LocalBase', 'integer', 'NULL', '', '', '',
+    'LastUpdatedBy', 'integer', 'NULL', '', '', '',
+    'LastUpdated', 'timestamp', 'NULL', '', '', '',
+    'Creator', 'integer', 'NULL', '', '', '',
+    'Created', 'timestamp', 'NULL', '', '', '',
+  ],
+  'primary_key' => 'id',
+  'unique' => [ ['Base', 'Target', 'Type'] ],
+  'index' => [  ],
+},
+
+'Users' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'Name', 'varchar', '', '120', '', '',
+    'Password', 'varchar', 'NULL', '40', '', '',
+    'Comments', 'blob', 'NULL', '', '', '',
+    'Signature', 'blob', 'NULL', '', '', '',
+    'EmailAddress', 'varchar', 'NULL', '120', '', '',
+    'FreeformContactInfo', 'blob', 'NULL', '', '', '',
+    'Organization', 'varchar', 'NULL', '200', '', '',
+    'Privileged', 'integer', 'NULL', '', '', '',
+    'RealName', 'varchar', 'NULL', '120', '', '',
+    'Nickname', 'varchar', 'NULL', '16', '', '',
+    'Lang', 'varchar', 'NULL', '16', '', '',
+    'EmailEncoding', 'varchar', 'NULL', '16', '', '',
+    'WebEncoding', 'varchar', 'NULL', '16', '', '',
+    'ExternalContactInfoId', 'varchar', 'NULL', '100', '', '',
+    'ContactInfoSystem', 'varchar', 'NULL', '30', '', '',
+    'ExternalAuthId', 'varchar', 'NULL', '100', '', '',
+    'AuthSystem', 'varchar', 'NULL', '30', '', '',
+    'Gecos', 'varchar', 'NULL', '16', '', '',
+    'HomePhone', 'varchar', 'NULL', '30', '', '',
+    'WorkPhone', 'varchar', 'NULL', '30', '', '',
+    'MobilePhone', 'varchar', 'NULL', '30', '', '',
+    'PagerPhone', 'varchar', 'NULL', '30', '', '',
+    'Address1', 'varchar', 'NULL', '200', '', '',
+    'Address2', 'varchar', 'NULL', '200', '', '',
+    'City', 'varchar', 'NULL', '100', '', '',
+    'State', 'varchar', 'NULL', '100', '', '',
+    'Zip', 'varchar', 'NULL', '16', '', '',
+    'Country', 'varchar', 'NULL', '50', '', '',
+    'Creator', 'integer', 'NULL', '', '', '',
+    'Created', 'timestamp', 'NULL', '', '', '',
+    'LastUpdatedBy', 'integer', 'NULL', '', '', '',
+    'LastUpdated', 'timestamp', 'NULL', '', '', '',
+    'Disabled', 'int2', '','','0','',
+  ],
+  'primary_key' => 'id',
+  'unique' => [ ['Name'] ],
+  'index' => [ ['Name'], 
+              ['id', 'EmailAddress'],
+              ['EmailAddress'] ],
+},
+
+'Tickets' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'EffectiveId', 'integer', 'NULL', '', '', '',
+    'Queue', 'integer', 'NULL', '', '', '',
+    'Type', 'varchar', 'NULL', '16', '', '',
+    'IssueStatement', 'integer', 'NULL', '', '', '',
+    'Resolution', 'integer', 'NULL', '', '', '',
+    'Owner', 'integer', 'NULL', '', '', '',
+    'Subject', 'varchar', 'NULL', '200', '[no subject]', '',
+    'InitialPriority', 'integer', 'NULL', '', '', '',
+    'FinalPriority', 'integer', 'NULL', '', '', '',
+    'Priority', 'integer', 'NULL', '', '', '',
+    'Status', 'varchar', 'NULL', '10', '', '',
+    'TimeWorked', 'integer', 'NULL', '', '', '',
+    'TimeLeft', 'integer', 'NULL', '', '', '',
+    'Told', 'timestamp', 'NULL', '', '', '',
+    'Starts', 'timestamp', 'NULL', '', '', '',
+    'Started', 'timestamp', 'NULL', '', '', '',
+    'Due', 'timestamp', 'NULL', '', '', '',
+    'Resolved', 'timestamp', 'NULL', '', '', '',
+    'LastUpdatedBy', 'integer', 'NULL', '', '', '',
+    'LastUpdated', 'timestamp', 'NULL', '', '', '',
+    'Creator', 'integer', 'NULL', '', '', '',
+    'Created', 'timestamp', 'NULL', '', '', '',
+    'Disabled', 'int2', '','','0','',
+  ],
+  'primary_key' => 'id',
+  'unique' => [ [] ],
+  'index' => [ ['Queue', 'Status'], 
+              ['Owner'], 
+              ['EffectiveId'], 
+              ['id', 'Status'], 
+              ['id', 'EffectiveId'] ],
+},
+
+'GroupMembers' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'GroupId', 'integer', 'NULL', '', '', '', #foreign key, Groups::id
+    'UserId', 'integer', 'NULL', '', '', '', #foreign key, Users::id
+  ],
+  'primary_key' => 'id',
+  'unique' => [ ['GroupId', 'UserId']  ],
+  'index' => [  ],
+},
+
+'Queues' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'Name', 'varchar', '', '120', '', '', #Textual 'name' for this queue
+    'Description', 'varchar', 'NULL', '120', '', '', #Textual descr. of this
+    #queue
+    'CorrespondAddress', 'varchar', 'NULL', '120', '', '',
+    'CommentAddress', 'varchar', 'NULL', '120', '', '',
+    'InitialPriority', 'integer', 'NULL', '', '', '',
+    'FinalPriority', 'integer', 'NULL', '', '', '',
+    'DefaultDueIn', 'integer', 'NULL', '', '', '',
+
+    'Creator', 'integer', 'NULL', '', '', '',
+    'Created', 'timestamp', 'NULL', '', '', '',
+    'LastUpdatedBy', 'integer', 'NULL', '', '', '',
+    'LastUpdated', 'timestamp', 'NULL', '', '', '',
+    'Disabled', 'int2', '','','0','',
+  ],
+  'primary_key' => 'id',
+  'unique' => [ ['Name'] ],
+  'index' => [  ],
+},
+
+'Transactions' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'EffectiveTicket', 'integer', 'NULL', '', '', '', 
+    'Ticket', 'integer', 'NULL', '', '', '',  #Foreign key Ticket::id
+    'TimeTaken', 'integer', 'NULL', '', '', '', #Time spent on this trans in min
+    'Type', 'varchar', 'NULL', '20', '', '',
+    'Field', 'varchar', 'NULL', '40', '', '', #If it's a "Set" transaction, what
+    #field was set.
+    'OldValue', 'varchar', 'NULL', '255', '', '', 
+    'NewValue', 'varchar', 'NULL', '255', '', '',
+    'Data', 'varchar', 'NULL', '100', '', '',
+
+
+    'Creator', 'integer', 'NULL', '', '', '',
+    'Created', 'timestamp', 'NULL', '', '', '',
+
+  ],
+  'primary_key' => 'id',
+  'unique' => [  ],
+  'index' => [ ['Ticket'], ['EffectiveTicket'] ],
+},
+
+'ScripActions' => {
+  'columns' => [
+               'id', 'serial', '', '', '', '',
+               'Name', 'varchar', 'NULL', '255', '', '',  # Alias
+               'Description', 'varchar', 'NULL', '255', '', '', #Textual description
+               'ExecModule', 'varchar', 'NULL', '60', '', '', #This calles RT::Action::___
+               'Argument', 'varchar', 'NULL', '255', '', '', #We can pass a single argument
+               #to the scrip. sometimes, it's who to send mail to.
+               'Creator', 'integer', 'NULL', '', '', '',
+               'Created', 'timestamp', 'NULL', '', '', '',
+               'LastUpdatedBy', 'integer', 'NULL', '', '', '',
+               'LastUpdated', 'timestamp', 'NULL', '', '', '',
+  ],
+  'primary_key' => 'id',
+  'unique' => [  ],
+  'index' => [  ],
+},
+
+'ScripConditions' => {
+  'columns' => [
+               'id', 'serial', '', '', '', '',
+               'Name', 'varchar', 'NULL', '255', '', '',  # Alias
+               'Description', 'varchar', 'NULL', '255', '', '', #Textual description
+               'ExecModule', 'varchar', 'NULL', '60', '', '', #This calles RT::Condition::
+               'Argument', 'varchar', 'NULL', '255', '', '', #We can pass a single argument
+               #to the scrip. sometimes, it's who to send mail to.
+               'ApplicableTransTypes', 'varchar', 'NULL', '60', '', '',#Transaction types this scrip
+               # acts on. comma or / delimited is just great.
+               'Creator', 'integer', 'NULL', '', '', '',
+               'Created', 'timestamp', 'NULL', '', '', '',
+               'LastUpdatedBy', 'integer', 'NULL', '', '', '',
+               'LastUpdated', 'timestamp', 'NULL', '', '', '',
+  ],
+  'primary_key' => 'id',
+  'unique' => [  ],
+  'index' => [  ],
+},
+'Scrips' => {
+                'columns' => [
+                              'id', 'serial', '', '', '', '',
+                              'ScripCondition', 'integer', 'NULL', '', '', '', #Foreign key ScripConditions::id
+                              'ScripAction', 'integer', 'NULL', '', '', '', #Foreign key ScripActions::id
+                              'Stage', 'varchar', 'NULL', '32','','', #What stage does this scrip
+                              #Happen in.  for now, everything is 'TransactionCreate',
+                              'Queue', 'integer', 'NULL', '', '', '', #Foreign key Queues::id
+                              'Template', 'integer', 'NULL', '', '', '', #Foreign key Templates::id
+                              
+                              'Creator', 'integer', 'NULL', '', '', '',
+                              'Created', 'timestamp', 'NULL', '', '', '',
+                              'LastUpdatedBy', 'integer', 'NULL', '', '', '',
+                              'LastUpdated', 'timestamp', 'NULL', '', '', '',
+                             ],
+                'primary_key' => 'id',
+                'unique' => [  ],
+                'index' => [  ],
+},
+
+'Attachments' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'TransactionId', 'integer', '', '', '', '', #Foreign key Transactions::Id
+    'Parent', 'integer', 'NULL', '', '', '', # Attachments::Id
+    'MessageId', 'varchar', 'NULL', '160', '', '', #RFC822 messageid, if any
+    'Subject', 'varchar', 'NULL', '255', '', '', 
+    'Filename', 'varchar', 'NULL', '255', '', '',
+    'ContentType', 'varchar', 'NULL', '80', '', '',
+    'ContentEncoding', 'varchar', 'NULL', '80', '', '',
+    'Content', 'long varbinary', 'NULL', '', '', '',
+    'Headers', 'long varbinary', 'NULL', '', '', '',
+
+    'Creator', 'integer', 'NULL', '', '', '',
+    'Created', 'timestamp', 'NULL', '', '', '',
+
+  ],
+  'primary_key' => 'id',
+  'unique' => [  ],
+  'index' => [ ['Parent'], ['TransactionId'], ['Parent', 'TransactionId'] ],
+},
+
+'Templates' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'Queue', 'integer', 'NOT NULL', '', '0', '',
+    'Name', 'varchar', '', '40', '', '',
+    'Description', 'varchar', 'NULL', '120', '', '',
+    'Type', 'varchar', 'NULL', '16', '','',
+    'Language', 'varchar', 'NULL', '16', '', '',
+    'TranslationOf', 'integer', 'NULL', '', '', '',
+    'Content', 'blob', 'NULL', '', '', '',
+    'LastUpdated', 'timestamp', 'NULL', '', '', '',
+    'LastUpdatedBy', 'integer', 'NULL', '', '', '',
+    'Creator', 'integer', 'NULL', '', '', '',
+    'Created', 'timestamp', 'NULL', '', '', '',
+  ],
+  'primary_key' => 'id',
+  'unique' => [ [''] ],
+  'index' => [  ],
+},
+
+'Keywords' => {
+  'columns' => [
+    'id', 'serial', '', '', '', '',
+    'Name', 'varchar', 'NOT NULL', '255', '', '',
+    'Description', 'varchar', 'NULL', '255', '', '',
+    'Parent', 'integer', 'NULL', '', '', '',
+    'Disabled', 'int2', '','','0','',
+],
+  'primary_key' => 'id',
+  'unique' => [ [ 'Name', 'Parent' ] ],
+  'index' => [ [ 'Name', ], [ 'Parent' ] ],
+},
+
+'ObjectKeywords' => {
+  'columns' =>  [
+    'id', 'serial', '', '', '', '',
+    'Keyword', 'integer', 'NOT NULL', '', '', '',
+    'KeywordSelect', 'integer', 'NOT NULL', '', '', '',
+    'ObjectType', 'varchar', 'NOT NULL', '32', '', '',
+    'ObjectId', 'integer', 'NOT NULL', '', '', '',
+  ],
+  'primary_key' => 'id',
+  'unique' => [ [  'ObjectId', 'ObjectType','KeywordSelect', 'Keyword' ] ],
+  'index' => [ [ 'ObjectId', 'ObjectType'  ] , ['Keyword'] ],
+
+},
+
+'KeywordSelects' => {
+  'columns' =>  [
+    'id', 'serial', '', '', '', '',
+    'Name','varchar','NULL','255','','',
+    'Keyword', 'integer', 'NULL', '', '', '',
+    'Single', 'integer', 'NULL', '', '', '',
+    'Depth', 'integer', 'NOT NULL', '', 0, '',
+    'ObjectType', 'varchar', 'NOT NULL',  '32', '', '',
+    'ObjectField', 'varchar', 'NULL', '32', '', '',
+    'ObjectValue', 'varchar', 'NULL', '255', '', '',
+    'Disabled', 'int2', '','','0','',
+  ],
+  'primary_key' => 'id',
+  'unique' => [ [ ] ],
+  'index' => [ [ 'Keyword' ], [ 'ObjectType', 'ObjectField', 'ObjectValue'] ],
+},
+
+};
diff --git a/rt/lib/MANIFEST b/rt/lib/MANIFEST
new file mode 100644 (file)
index 0000000..cda386b
--- /dev/null
@@ -0,0 +1,57 @@
+MANIFEST
+MANIFEST.SKIP
+Makefile.PL
+RT.pm
+test.pl
+RT/ACE.pm
+RT/ACL.pm
+RT/Action/Generic.pm
+RT/Action/NotifyAsComment.pm
+RT/Action/OpenDependent.pm
+RT/Action/SendEmail.pm
+RT/Action/StallDependent.pm
+RT/Action/Notify.pm
+RT/Action/ResolveMembers.pm
+RT/Attachment.pm
+RT/Attachments.pm
+RT/Condition/AnyTransaction.pm
+RT/Condition/Generic.pm
+RT/Condition/NewDependency.pm
+RT/CurrentUser.pm
+RT/Date.pm
+RT/EasySearch.pm
+RT/Group.pm
+RT/GroupMember.pm
+RT/GroupMembers.pm
+RT/Groups.pm
+RT/Handle.pm
+RT/Interface/CLI.pm
+RT/Interface/Email.pm
+RT/Interface/Web.pm
+RT/Keyword.pm
+RT/Keywords.pm
+RT/KeywordSelect.pm
+RT/KeywordSelects.pm
+RT/Link.pm
+RT/Links.pm
+RT/ObjectKeyword.pm
+RT/ObjectKeywords.pm
+RT/Queue.pm
+RT/Queues.pm
+RT/Record.pm
+RT/Scrip.pm
+RT/Scrips.pm
+RT/ScripAction.pm
+RT/ScripActions.pm
+RT/ScripCondition.pm
+RT/ScripConditions.pm
+RT/Template.pm
+RT/Templates.pm
+RT/Ticket.pm
+RT/Tickets.pm
+RT/Transaction.pm
+RT/Transactions.pm
+RT/User.pm
+RT/Users.pm
+RT/Watcher.pm
+RT/Watchers.pm
diff --git a/rt/lib/MANIFEST.SKIP b/rt/lib/MANIFEST.SKIP
new file mode 100644 (file)
index 0000000..ae335e7
--- /dev/null
@@ -0,0 +1 @@
+CVS/
diff --git a/rt/lib/Makefile.PL b/rt/lib/Makefile.PL
new file mode 100644 (file)
index 0000000..c0e1af2
--- /dev/null
@@ -0,0 +1,49 @@
+use ExtUtils::MakeMaker;
+# See lib/ExtUtils/MakeMaker.pm for details of how to influence
+# the contents of the Makefile that is written.
+WriteMakefile(
+    'NAME'      => 'RT',
+    'VERSION_FROM' => 'RT.pm', # finds $VERSION
+    'PREREQ_PM' => {
+                     'DBI'                 => 1.16,
+                     'DBIx::SearchBuilder' => '0.48',
+                     'Date::Parse'         => 0,
+                     'Date::Format'        => 0,
+                     'MIME::Entity'        => 5.108,
+                     'Mail::Mailer'        => '1.20',
+                     'Log::Dispatch'       => 1.6,
+                     'HTML::Entities'      => 0,
+                     'Text::Wrapper'       => 0,
+                     'Text::Template'      => 0,
+                    'Getopt::Long'        => 2.24,
+                   },
+);
+
+     {
+                   package MY;
+                   sub top_targets {
+                       my($self) = @_;
+                       my $out = "POD2TEST_EXE = pod2test\n";
+
+                       $out .= $self->SUPER::top_targets(@_);
+                       # $out =~ s/^(pure_all\b.*)/$1 testifypods/m;
+
+                       $out .= "\n\ntestifypods : \n";
+
+                       my @pods = (keys %{$self->{MAN1PODS}},
+                                    keys %{$self->{MAN3PODS}});
+
+                       foreach my $pod (@pods) {
+                           (my $test = $pod) =~ s/\.(pm|pod)$//;
+                           $test =~ s/^lib\W//;
+                           $test =~ s/\W/-/;
+                          $test =~ s/\//__/g;
+                           $test = "autogen-$test.t";
+                           $out .= "\t$self->{NOECHO}\$(POD2TEST_EXE) ".
+                                   "$pod t/$test \n";
+                       }
+
+                       return $out;
+                   }
+               }
+
diff --git a/rt/lib/RT.pm b/rt/lib/RT.pm
new file mode 100644 (file)
index 0000000..1cfc428
--- /dev/null
@@ -0,0 +1,155 @@
+package RT;
+use RT::Handle;
+use RT::CurrentUser;
+use strict;
+
+use vars qw($VERSION $SystemUser $Nobody $Handle $Logger);
+
+$VERSION = '!!RT_VERSION!!';
+
+=head1 NAME
+
+       RT - Request Tracker
+
+=head1 SYNOPSIS
+
+       A fully featured request tracker package
+       
+
+=head1 DESCRIPTION
+
+
+=cut
+
+sub Init {
+    #Get a database connection
+    $Handle = new RT::Handle($RT::DatabaseType);
+    $Handle->Connect();
+    
+    
+    #RT's system user is a genuine database user. its id lives here
+    $SystemUser = new RT::CurrentUser();
+    $SystemUser->LoadByName('RT_System');
+    
+    #RT's "nobody user" is a genuine database user. its ID lives here.
+    $Nobody = new RT::CurrentUser();
+    $Nobody->LoadByName('Nobody');
+   
+   InitLogging(); 
+}
+
+=head2 InitLogging
+
+Create the RT::Logger object. 
+
+=cut
+sub InitLogging {
+
+    # We have to set the record seperator ($, man perlvar)
+    # or Log::Dispatch starts getting
+    # really pissy, as some other module we use unsets it.
+
+    $, = '';
+    use Log::Dispatch 1.6;
+    use Log::Dispatch::File;
+    use Log::Dispatch::Screen;
+
+    $Logger=Log::Dispatch->new();
+    
+    if ($RT::LogToFile) {
+       my $filename = $RT::LogToFileNamed || "$RT::LogDir/rt.log";
+
+         $Logger->add(Log::Dispatch::File->new
+                      ( name=>'rtlog',
+                        min_level=> $RT::LogToFile,
+                        filename=> $filename,
+                        mode=>'append',
+            callbacks => sub {my %p=@_; return "[".gmtime(time)."] [".$p{level}."]: $p{message}\n"}
+
+                      ));
+    }
+    if ($RT::LogToScreen) {
+       $Logger->add(Log::Dispatch::Screen->new
+                    ( name => 'screen',
+                      min_level => $RT::LogToScreen,
+                      stderr => 1
+                    ));
+    }
+# {{{ Signal handlers
+
+## This is the default handling of warnings and die'ings in the code
+## (including other used modules - maybe except for errors catched by
+## Mason).  It will log all problems through the standard logging
+## mechanism (see above).
+
+$SIG{__WARN__} = sub {$RT::Logger->warning($_[0])};
+
+#When we call die, trap it and log->crit with the value of the die.
+
+$SIG{__DIE__}  = sub {
+    unless ($^S || !defined $^S ) {
+        $RT::Logger->crit("$_[0]");
+        exit(-1);
+    }
+    else {
+        #Get out of here if we're in an eval
+        die $_[0];
+    }
+};
+
+# }}}
+
+}
+
+# }}}
+
+
+sub SystemUser {
+    return($SystemUser);
+}      
+
+sub Nobody {
+    return ($Nobody);
+}
+
+
+=head2 DropSetGIDPermissions
+
+Drops setgid permissions.
+
+=cut
+
+sub DropSetGIDPermissions {
+    # Now that we got the config read in, we have the database 
+    # password and don't need to be setgid
+    # make the effective group the real group
+    $) = $(;
+}
+
+
+=head1 NAME
+
+RT - Request Tracker
+
+=head1 SYNOPSIS
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+
+ok ($RT::Nobody->Name() eq 'Nobody', "Nobody is nobody");
+ok ($RT::Nobody->Name() ne 'root', "Nobody isn't named root");
+ok ($RT::SystemUser->Name() eq 'RT_System', "The system user is RT_System");
+ok ($RT::SystemUser->Name() ne 'noname', "The system user isn't noname");
+
+
+=end testing
+
+=cut
+
+1;
diff --git a/rt/lib/RT/ACE.pm b/rt/lib/RT/ACE.pm
new file mode 100755 (executable)
index 0000000..d4681cf
--- /dev/null
@@ -0,0 +1,774 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/ACE.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::ACE - RT\'s ACE object
+
+=head1 SYNOPSIS
+
+  use RT::ACE;
+  my $ace = new RT::ACE($CurrentUser);
+
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::ACE);
+
+=end testing
+
+=cut
+
+package RT::ACE;
+use RT::Record;
+@ISA= qw(RT::Record);
+
+use vars qw (%SCOPES
+            %QUEUERIGHTS
+            %SYSTEMRIGHTS
+            %LOWERCASERIGHTNAMES
+           ); 
+
+%SCOPES = (
+          System => 'System-level right',
+          Queue => 'Queue-level right'
+         );
+
+# {{{ Descriptions of rights
+
+# Queue rights are the sort of queue rights that can only be granted
+# to real people or groups
+%QUEUERIGHTS = ( 
+               SeeQueue => 'Can this principal see this queue',
+               AdminQueue => 'Create, delete and modify queues', 
+               ShowACL => 'Display Access Control List',
+               ModifyACL => 'Modify Access Control List',
+               ModifyQueueWatchers => 'Modify the queue watchers',
+                AdminKeywordSelects => 'Create, delete and modify keyword selections',
+
+               
+               ModifyTemplate => 'Modify email templates for this queue',
+               ShowTemplate => 'Display email templates for this queue',
+               ModifyScrips => 'Modify Scrips for this queue',
+               ShowScrips => 'Display Scrips for this queue',
+
+               ShowTicket => 'Show ticket summaries',
+               ShowTicketComments => 'Show ticket private commentary',
+
+               Watch => 'Sign up as a ticket Requestor or ticket or queue Cc',
+               WatchAsAdminCc => 'Sign up as a ticket or queue AdminCc',
+               CreateTicket => 'Create tickets in this queue',
+               ReplyToTicket => 'Reply to tickets',
+               CommentOnTicket => 'Comment on tickets',
+               OwnTicket => 'Own tickets',
+               ModifyTicket => 'Modify tickets',
+               DeleteTicket => 'Delete tickets'
+
+              );       
+
+
+# System rights are rights granted to the whole system
+%SYSTEMRIGHTS = (
+                SuperUser => 'Do anything and everything',
+               AdminKeywords => 'Creatte, delete and modify keywords',  
+               AdminGroups => 'Create, delete and modify groups',
+               AdminUsers => 'Create, Delete and Modify users',
+               ModifySelf => 'Modify one\'s own RT account',
+
+               );
+
+# }}}
+
+# {{{ Descriptions of principals
+
+%TICKET_METAPRINCIPALS = ( Owner => 'The owner of a ticket',
+                                  Requestor => 'The requestor of a ticket',
+                                  Cc => 'The CC of a ticket',
+                                      AdminCc => 'The administrative CC of a ticket',
+                        );
+
+# }}}
+
+# {{{ We need to build a hash of all rights, keyed by lower case names
+
+#since you can't do case insensitive hash lookups
+
+foreach $right (keys %QUEUERIGHTS) {
+    $LOWERCASERIGHTNAMES{lc $right}=$right;
+}
+foreach $right (keys %SYSTEMRIGHTS) {
+    $LOWERCASERIGHTNAMES{lc $right}=$right;
+}
+
+# }}}
+
+# {{{ sub _Init
+sub _Init  {
+  my $self = shift;
+  $self->{'table'} = "ACL";
+  return($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub LoadByValues
+
+=head2 LoadByValues PARAMHASH
+
+Load an ACE by specifying a paramhash with the following fields:
+
+              PrincipalId => undef,
+             PrincipalType => undef,
+             RightName => undef,
+             RightScope => undef,
+             RightAppliesTo => undef,
+
+=cut
+
+sub LoadByValues {
+  my $self = shift;
+  my %args = (PrincipalId => undef,
+             PrincipalType => undef,
+             RightName => undef,
+             RightScope => undef,
+             RightAppliesTo => undef,
+             @_);
+  
+  $self->LoadByCols (PrincipalId => $args{'PrincipalId'},
+                    PrincipalType => $args{'PrincipalType'},
+                    RightName => $args{'RightName'},
+                    RightScope => $args{'RightScope'},
+                    RightAppliesTo => $args{'RightAppliesTo'}
+                   );
+  
+  #If we couldn't load it.
+  unless ($self->Id) {
+      return (0, "ACE not found");
+  }
+  # if we could
+  return ($self->Id, "ACE Loaded");
+  
+}
+
+# }}}
+
+# {{{ sub Create
+
+=head2 Create <PARAMS>
+
+PARAMS is a parameter hash with the following elements:
+
+   PrincipalType => "Queue"|"User"
+   PrincipalId => an intentifier you can use to ->Load a user or group
+   RightName => the name of a right. in any case
+   RightScope => "System" | "Queue"
+   RightAppliesTo => a queue id or undef
+
+=cut
+
+sub Create {
+    my $self = shift;
+    my %args = ( PrincipalId => undef,
+                PrincipalType => undef,
+                RightName => undef,
+                RightScope => undef,
+                RightAppliesTo => undef,
+                @_
+              );
+    
+    # {{{ Validate the principal
+    my ($princ_obj);
+    if ($args{'PrincipalType'} eq 'User') {
+       $princ_obj = new RT::User($RT::SystemUser);
+       
+    }  
+    elsif ($args{'PrincipalType'} eq 'Group') {
+       require RT::Group;
+       $princ_obj = new RT::Group($RT::SystemUser);
+    }
+    else {
+       return (0, 'Principal type '.$args{'PrincipalType'} . ' is invalid.');
+    }  
+    
+    $princ_obj->Load($args{'PrincipalId'});
+    my $princ_id = $princ_obj->Id();
+    
+    unless ($princ_id) {
+       return (0, 'Principal '.$args{'PrincipalId'}.' not found.');
+    }
+
+    # }}}
+    
+    #TODO allow loading of queues by name.    
+    
+    # {{{ Check the ACL
+    if ($args{'RightScope'} eq 'System') {
+       
+       unless ($self->CurrentUserHasSystemRight('ModifyACL')) {
+           $RT::Logger->error("Permission Denied.");
+           return(undef);
+       }
+    }
+    
+    elsif ($args{'RightScope'} eq 'Queue') {
+       unless ($self->CurrentUserHasQueueRight( Queue => $args{'RightAppliesTo'},
+                                                Right => 'ModifyACL')) {
+           return (0, 'Permission Denied.');
+       }
+       
+       
+       
+       
+    }
+    #If it's not a scope we recognise, something scary is happening.
+    else {
+       $RT::Logger->err("RT::ACE->Create got a scope it didn't recognize: ".
+                        $args{'RightScope'}." Bailing. \n");
+       return(0,"System error. Unable to grant rights.");
+    }
+
+    # }}}
+
+    # {{{ Canonicalize and check the right name
+    $args{'RightName'} = $self->CanonicalizeRightName($args{'RightName'});
+    
+    #check if it's a valid RightName
+    if ($args{'RightScope'} eq 'Queue') {
+       unless (exists $QUEUERIGHTS{$args{'RightName'}}) {
+           return(0, 'Invalid right');
+       }       
+       }       
+    elsif ($args{'RightScope' eq 'System'}) {
+       unless (exists $SYSTEMRIGHTS{$args{'RightName'}}) {
+           return(0, 'Invalid right');
+       }                   
+    }  
+    # }}}
+    
+    # Make sure the right doesn't already exist.
+    $self->LoadByCols (PrincipalId => $princ_id,
+                      PrincipalType => $args{'PrincipalType'},
+                      RightName => $args{'RightName'},
+                      RightScope => $args {'RightScope'},
+                      RightAppliesTo => $args{'RightAppliesTo'}
+                     );
+    if ($self->Id) {
+       return (0, 'That user already has that right');
+    }  
+
+    my $id = $self->SUPER::Create( PrincipalId => $princ_id,
+                                  PrincipalType => $args{'PrincipalType'},
+                                  RightName => $args{'RightName'},
+                                  RightScope => $args {'RightScope'},
+                                  RightAppliesTo => $args{'RightAppliesTo'}
+                                );
+    
+    
+    if ($id > 0 ) {
+       return ($id, 'Right Granted');
+    }
+    else {
+       $RT::Logger->err('System error. right not granted.');
+       return(0, 'System Error. right not granted');
+    }
+}
+
+# }}}
+
+
+# {{{ sub Delete 
+
+=head2 Delete
+
+Delete this object.
+
+=cut
+
+sub Delete {
+    my $self = shift;
+    
+    unless ($self->CurrentUserHasRight('ModifyACL')) {
+       return (0, 'Permission Denied');
+    }  
+    
+    
+    my ($val,$msg) = $self->SUPER::Delete(@_);
+    if ($val) {
+       return ($val, 'ACE Deleted');
+    }  
+    else {
+       return (0, 'ACE could not be deleted');
+    }
+}
+
+# }}}
+
+# {{{ sub _BootstrapRight 
+
+=head2 _BootstrapRight
+
+Grant a right with no error checking and no ACL. this is _only_ for 
+installation. If you use this routine without jesse@fsck.com's explicit 
+written approval, he will hunt you down and make you spend eternity
+translating mozilla's code into FORTRAN or intercal.
+
+=cut
+
+sub _BootstrapRight {
+    my $self = shift;
+    my %args = @_;
+
+    my $id = $self->SUPER::Create( PrincipalId => $args{'PrincipalId'},
+                                  PrincipalType => $args{'PrincipalType'},
+                                  RightName => $args{'RightName'},
+                                  RightScope => $args {'RightScope'},
+                                  RightAppliesTo => $args{'RightAppliesTo'}
+                                );
+    
+    if ($id > 0 ) {
+       return ($id);
+    }
+    else {
+       $RT::Logger->err('System error. right not granted.');
+       return(undef);
+    }
+    
+}
+
+# }}}
+
+# {{{ sub CanonicalizeRightName
+
+=head2 CanonicalizeRightName <RIGHT>
+
+Takes a queue or system right name in any case and returns it in
+the correct case. If it's not found, will return undef.
+
+=cut
+
+sub CanonicalizeRightName {
+    my $self = shift;
+    my $right = shift;
+    $right = lc $right;
+    if (exists $LOWERCASERIGHTNAMES{"$right"}) {
+       return ($LOWERCASERIGHTNAMES{"$right"});
+    }
+    else {
+       return (undef);
+    }
+}
+
+# }}}
+
+# {{{ sub QueueRights
+
+=head2 QueueRights
+
+Returns a hash of all the possible rights at the queue scope
+
+=cut
+
+sub QueueRights {
+        return (%QUEUERIGHTS);
+}
+
+# }}}
+
+# {{{ sub SystemRights
+
+=head2 SystemRights
+
+Returns a hash of all the possible rights at the system scope
+
+=cut
+
+sub SystemRights {
+       return (%SYSTEMRIGHTS);
+}
+
+
+# }}}
+
+# {{{ sub _Accessible 
+
+sub _Accessible  {
+  my $self = shift;  
+  my %Cols = (
+             PrincipalId => 'read/write',
+             PrincipalType => 'read/write',
+             RightName => 'read/write', 
+             RightScope => 'read/write',
+             RightAppliesTo => 'read/write'
+           );
+  return($self->SUPER::_Accessible(@_, %Cols));
+}
+# }}}
+
+# {{{ sub AppliesToObj
+
+=head2 AppliesToObj
+
+If the AppliesTo is a queue, returns the queue object. If it's 
+the system object, returns undef. If the user has no rights, returns undef.
+
+=cut
+
+sub AppliesToObj {
+    my $self = shift;
+    if ($self->RightScope eq 'Queue') {
+       my $appliesto_obj = new RT::Queue($self->CurrentUser);
+       $appliesto_obj->Load($self->RightAppliesTo);
+       return($appliesto_obj);
+    }
+    elsif ($self->RightScope eq 'System') {
+       return (undef);
+    }  
+    else {
+       $RT::Logger->warning("$self -> AppliesToObj called for an object ".
+                            "of an unknown scope:" . $self->RightScope);
+       return(undef);
+    }
+}      
+
+# }}}
+
+# {{{ sub PrincipalObj
+
+=head2 PrincipalObj
+
+If the AppliesTo is a group, returns the group object.
+If the AppliesTo is a user, returns the user object.
+Otherwise, it logs a warning and returns undef.
+
+=cut
+
+sub PrincipalObj {
+    my $self = shift;
+    my ($princ_obj);
+
+    if ($self->PrincipalType eq 'Group') {
+       use RT::Group;
+       $princ_obj = new RT::Group($self->CurrentUser);
+    }
+    elsif ($self->PrincipalType eq 'User') {
+       $princ_obj = new RT::User($self->CurrentUser);
+    }
+    else {
+       $RT::Logger->warning("$self -> PrincipalObj called for an object ".
+                            "of an unknown principal type:" . 
+                            $self->PrincipalType ."\n");
+       return(undef);
+    }
+    
+    $princ_obj->Load($self->PrincipalId);
+    return($princ_obj);
+
+}      
+
+# }}}
+
+# {{{ ACL related methods
+
+# {{{ sub _Set
+
+sub _Set {
+  my $self = shift;
+  return (0, "ACEs can only be created and deleted.");
+}
+
+# }}}
+
+# {{{ sub _Value
+
+sub _Value {
+    my $self = shift;
+
+    unless ($self->CurrentUserHasRight('ShowACL')) {
+       return (undef);
+    }
+
+    return ($self->__Value(@_));
+}
+
+# }}}
+
+
+# {{{ sub CurrentUserHasQueueRight 
+
+=head2 CurrentUserHasQueueRight ( Queue => QUEUEID, Right => RIGHTNANAME )
+
+Check to see whether the current user has the specified right for the specified queue.
+
+=cut
+
+sub CurrentUserHasQueueRight {
+    my $self = shift;
+    my %args = (Queue => undef,
+               Right => undef,
+               @_
+               );
+    return ($self->HasRight( Right => $args{'Right'},
+                            Principal => $self->CurrentUser->UserObj,
+                            Queue => $args{'Queue'}));
+}
+
+# }}}
+
+# {{{ sub CurrentUserHasSystemRight 
+=head2 CurrentUserHasSystemRight RIGHTNAME
+
+Check to see whether the current user has the specified right for the 'system' scope.
+
+=cut
+
+sub CurrentUserHasSystemRight {
+    my $self = shift;
+    my $right = shift;
+    return ($self->HasRight( Right => $right,
+                            Principal => $self->CurrentUser->UserObj,
+                            System => 1
+                          ));
+}
+
+
+# }}}
+
+# {{{ sub CurrentUserHasRight
+
+=item CurrentUserHasRight RIGHT 
+Takes a rightname as a string.
+
+Helper menthod for HasRight. Presets Principal to CurrentUser then 
+calls HasRight.
+
+=cut
+
+sub CurrentUserHasRight {
+    my $self = shift;
+    my $right = shift;
+    return ($self->HasRight( Principal => $self->CurrentUser->UserObj,
+                             Right => $right,
+                          ));
+}
+
+# }}}
+
+# {{{ sub HasRight
+
+=item HasRight
+
+Takes a param-hash consisting of "Right" and "Principal"  Principal is 
+an RT::User object or an RT::CurrentUser object. "Right" is a textual
+Right string that applies to KeywordSelects
+
+=cut
+
+sub HasRight {
+    my $self = shift;
+    my %args = ( Right => undef,
+                 Principal => undef,
+                Queue => undef,
+                System => undef,
+                 @_ ); 
+
+    #If we're explicitly specifying a queue, as we need to do on create
+    if (defined $args{'Queue'}) {
+       return ($args{'Principal'}->HasQueueRight(Right => $args{'Right'},
+                                                 Queue => $args{'Queue'}));
+    }
+    #else if we're specifying to check a system right
+    elsif ((defined $args{'System'}) and (defined $args{'Right'})) {
+        return( $args{'Principal'}->HasSystemRight( $args{'Right'} ));
+    }  
+    
+    elsif ($self->__Value('RightScope') eq 'System') {
+       return $args{'Principal'}->HasSystemRight($args{'Right'});
+    }
+    elsif ($self->__Value('RightScope') eq 'Queue') {
+       return $args{'Principal'}->HasQueueRight( Queue => $self->__Value('RightAppliesTo'),
+                                                 Right => $args{'Right'} );
+    }  
+    else {
+       $RT::Logger->warning("$self: Trying to check an acl for a scope we ".
+                            "don't understand:" . $self->__Value('RightScope') ."\n");
+       return undef;
+    }
+
+
+
+}
+# }}}
+
+# }}}
+
+1;
+
+__DATA__
+
+# {{{ POD
+
+=head1 Out of date docs
+
+=head2 Table Structure
+
+PrincipalType, PrincipalId, Right,Scope,AppliesTo
+
+=head1 The docs are out of date. so you know.
+
+=head1 Scopes
+
+Scope is the scope of the right granted, not the granularity of the grant.
+For example, Queue and Ticket rights are both granted for a "queue." 
+Rights with a scope of 'System' don't have an AppliesTo. (They're global).
+Rights with a scope of "Queue" are rights that act on a queue.
+Rights with a scope of "System" are rights that act on some other aspect
+of the system.
+
+
+=item Queue
+=item System
+
+
+=head1 Rights
+
+=head2 Scope: Queue
+
+=head2 Queue rights that apply to a ticket within a queue
+
+Create Ticket in <queue>
+
+        Name: Create
+       Principals: <user> <group>
+Display Ticket Summary in <queue>
+
+       Name: Show
+       Principals: <user> <group> Owner Requestor Cc AdminCc
+
+Display Ticket History  <queue>
+
+       Name: ShowHistory
+       Principals: <user> <group> Owner Requestor Cc AdminCc
+
+Display Ticket Private Comments  <queue>
+
+       Name: ShowComments
+       Principals: <user> <group> Owner Requestor Cc AdminCc
+
+Reply to Ticket in <queue>
+
+       Name: Reply
+       Principals: <user> <group> Owner Requestor Cc AdminCc
+
+Comment on Ticket in <queue>
+
+       Name: Comment
+       Principals: <user> <group> Owner Requestor Cc AdminCc
+
+Modify Ticket in <queue>
+
+       Name: Modify
+       Principals: <user> <group> Owner Requestor Cc AdminCc
+
+Delete Tickets in <queue>
+
+       Name: Delete
+       Principals: <user> <group> Owner Requestor Cc AdminCc
+
+
+=head2 Queue Rights that apply to a whole queue
+
+These rights can only be granted to "real people"
+
+List Tickets in <queue>
+
+       Name: ListQueue
+       Principals: <user> <group>
+
+Know that <queue> exists
+    
+    Name: See
+    Principals: <user> <group>
+
+Display queue settings
+
+    Name: Explore
+    Principals: <user> <group>
+
+Modify Queue Watchers for <queue>
+
+       Name: ModifyQueueWatchers
+       Principals: <user> <group>
+
+Modify Queue Attributes for <queue> 
+
+       Name: ModifyQueue
+       Principals: <user> <group>
+
+Modify Queue ACL for queue <queue>
+
+       Name: ModifyACL
+       Principals: <user> <group>
+
+
+=head2 Rights that apply to the System scope
+
+=head2 SystemRights
+
+Create Queue
+  
+        Name: CreateQueue
+       Principals: <user> <group>
+Delete Queue
+  
+        Name: DeleteQueue
+       Principals: <user> <group>
+
+Create Users
+  
+        Name: CreateUser
+       Principals: <user> <group>
+
+Delete Users
+  
+        Name: DeleteUser
+       Principals: <user> <group>
+  
+Modify Users
+  
+        Name: ModifyUser
+       Principals: <user> <group>
+
+Modify Self
+        Name: ModifySelf
+       Principals: <user> <group>
+
+Browse Users
+
+        Name: BrowseUsers (NOT IMPLEMENTED in 2.0)
+       Principals: <user> <group>
+
+Modify Self
+                   
+       Name: ModifySelf
+       Principals: <user> <group>
+
+Modify System ACL
+
+       Name: ModifyACL           
+       Principals: <user> <group>
+
+=head1 The Principal Side of the ACE
+
+=head2 PrincipalTypes,PrincipalIds in our Neighborhood
+
+  User,<userid>
+  Group,<groupip>
+  Everyone,NULL
+
+=cut
+
+# }}}
diff --git a/rt/lib/RT/ACL.pm b/rt/lib/RT/ACL.pm
new file mode 100755 (executable)
index 0000000..444a4c2
--- /dev/null
@@ -0,0 +1,308 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/ACL.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Distributed under the terms of the GNU GPL
+# Copyright (c) 2000 Jesse Vincent <jesse@fsck.com>
+
+=head1 NAME
+
+  RT::ACL - collection of RT ACE objects
+
+=head1 SYNOPSIS
+
+  use RT::ACL;
+my $ACL = new RT::ACL($CurrentUser);
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::ACL);
+
+=end testing
+
+=cut
+
+package RT::ACL;
+use RT::EasySearch;
+use RT::ACE;
+@ISA= qw(RT::EasySearch);
+
+# {{{ sub _Init 
+sub _Init  {
+  my $self = shift;
+  $self->{'table'} = "ACL";
+  $self->{'primary_key'} = "id";
+  return ( $self->SUPER::_Init(@_));
+  
+}
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem  {
+  my $self = shift;
+  return(RT::ACE->new($self->CurrentUser));
+}
+# }}}
+
+=head2 Next
+
+Hand out the next ACE that was found
+
+=cut
+
+# {{{ sub Next 
+sub Next {
+    my $self = shift;
+    
+    my $ACE = $self->SUPER::Next();
+    if ((defined($ACE)) and (ref($ACE))) {
+       
+       if ( $ACE->CurrentUserHasRight('ShowACL') or
+            $ACE->CurrentUserHasRight('ModifyACL')
+          ) {
+           return($ACE);
+       }
+       
+       #If the user doesn't have the right to show this ACE
+       else {  
+           return($self->Next());
+       }
+    }
+    #if there never was any ACE
+    else {
+       return(undef);
+    }  
+    
+}
+
+# }}}
+
+
+=head1 Limit the ACL to a specific scope
+
+There are two real scopes right now:
+
+=item Queue is for rights that apply to a single queue
+
+=item System is for rights that apply to the System (rights that aren't queue related)
+
+
+=head2 LimitToQueue
+
+Takes a single queueid as its argument.
+
+Limit the ACL to just a given queue when supplied with an integer queue id.
+
+=cut
+
+sub LimitToQueue {
+    my $self = shift;
+    my $queue = shift;
+    
+    
+    
+    $self->Limit( FIELD =>'RightScope',
+                 ENTRYAGGREGATOR => 'OR',
+                 VALUE => 'Queue');
+    $self->Limit( FIELD =>'RightScope',
+                 ENTRYAGGREGATOR => 'OR',
+               VALUE => 'Ticket');
+    
+    $self->Limit(ENTRYAGGREGATOR => 'OR',
+                FIELD => 'RightAppliesTo',
+                VALUE => $queue );
+  
+}
+
+
+=head2 LimitToSystem()
+
+Limit the ACL to system rights
+
+=cut 
+
+sub LimitToSystem {
+  my $self = shift;
+  
+  $self->Limit( FIELD =>'RightScope',
+               VALUE => 'System');
+}
+
+
+=head2 LimitRightTo
+
+Takes a single RightName as its only argument.
+Limits the search to the right $right.
+$right is a right listed in perldoc RT::ACE
+
+=cut
+
+sub LimitRightTo {
+  my $self = shift;
+  my $right = shift;
+  
+  $self->Limit(ENTRYAGGREGATOR => 'OR',
+              FIELD => 'RightName',
+              VALUE => $right );
+  
+}
+
+=head1 Limit to a specifc set of principals
+
+=head2 LimitPrincipalToUser
+
+Takes a single userid as its only argument.
+Limit the ACL to a just a specific user.
+
+=cut
+
+sub LimitPrincipalToUser {
+  my $self = shift;
+  my $user = shift;
+  
+  $self->Limit(ENTRYAGGREGATOR => 'OR',
+              FIELD => 'PrincipalType',
+              VALUE => 'User' );
+  
+  $self->Limit(ENTRYAGGREGATOR => 'OR',
+              FIELD => 'PrincipalId',
+              VALUE => $user );
+  
+}
+
+
+=head2 LimitPrincipalToGroup
+
+Takes a single group as its only argument.
+Limit the ACL to just a specific group.
+
+=cut
+  
+sub LimitPrincipalToGroup {
+  my $self = shift;
+  my $group = shift;
+  
+  $self->Limit(ENTRYAGGREGATOR => 'OR',
+              FIELD => 'PrincipalType',
+              VALUE => 'Group' );
+
+  $self->Limit(ENTRYAGGREGATOR => 'OR',
+              FIELD => 'PrincipalId',
+              VALUE => $group );
+
+}
+
+=head2 LimitPrincipalToType($type)
+
+Takes a single argument, $type.
+Limit the ACL to just a specific principal type
+
+$type is one of:
+  TicketOwner
+  TicketRequestor
+  TicketCc
+  TicketAdminCc
+  Everyone
+  User
+  Group
+
+=cut
+
+sub LimitPrincipalToType {
+  my $self=shift;
+  my $type=shift;  
+  $self->Limit(ENTRYAGGREGATOR => 'OR',
+               FIELD => 'PrincipalType',
+               VALUE => $type );
+}
+
+
+=head2 LimitPrincipalToId 
+
+Takes a single argument, the numeric Id of the principal to limit this ACL to. Repeated calls to this 
+function will broaden the scope of the search to include all principals listed.
+
+=cut
+
+sub LimitPrincipalToId {
+    my $self = shift;
+    my $id = shift;
+
+    if ($id =~ /^\d+$/) {
+       $self->Limit(ENTRYAGGREGATOR => 'OR',
+                    FIELD => 'PrincipalId',
+                    VALUE => $id );
+    }
+    else {
+       $RT::Logger->warn($self."->LimitPrincipalToId called with '$id' as an id");
+       return undef;
+    }
+}
+
+
+#wrap around _DoSearch  so that we can build the hash of returned
+#values 
+sub _DoSearch {
+    my $self = shift;
+   # $RT::Logger->debug("Now in ".$self."->_DoSearch");
+    my $return = $self->SUPER::_DoSearch(@_);
+  #  $RT::Logger->debug("In $self ->_DoSearch. return from SUPER::_DoSearch was $return\n");
+    $self->_BuildHash();
+    return ($return);
+}
+
+
+#Build a hash of this ACL's entries.
+sub _BuildHash {
+    my $self = shift;
+
+    while (my $entry = $self->Next) {
+       my $hashkey = $entry->RightScope . "-" .
+                             $entry->RightAppliesTo . "-" . 
+                             $entry->RightName . "-" .
+                             $entry->PrincipalId . "-" .
+                             $entry->PrincipalType;
+
+        $self->{'as_hash'}->{"$hashkey"} =1;
+
+    }
+}
+
+
+# {{{ HasEntry
+
+=head2 HasEntry
+
+=cut
+
+sub HasEntry {
+
+    my $self = shift;
+    my %args = ( RightScope => undef,
+                 RightAppliesTo => undef,
+                 RightName => undef,
+                 PrincipalId => undef,
+                 PrincipalType => undef,
+                 @_ );
+
+    #if we haven't done the search yet, do it now.
+    $self->_DoSearch();
+
+    if ($self->{'as_hash'}->{ $args{'RightScope'} . "-" .
+                             $args{'RightAppliesTo'} . "-" . 
+                             $args{'RightName'} . "-" .
+                             $args{'PrincipalId'} . "-" .
+                             $args{'PrincipalType'}
+                            } == 1) {
+       return(1);
+    }
+    else {
+       return(undef);
+    }
+}
+
+# }}}
+1;
diff --git a/rt/lib/RT/Action/Autoreply.pm b/rt/lib/RT/Action/Autoreply.pm
new file mode 100755 (executable)
index 0000000..624888e
--- /dev/null
@@ -0,0 +1,64 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Action/Autoreply.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+package RT::Action::Autoreply;
+require RT::Action::SendEmail;
+@ISA = qw(RT::Action::SendEmail);
+
+
+# {{{ sub SetRecipients
+
+=head2 SetRecipients
+
+Sets the recipients of this message to this ticket's Requestor.
+
+=cut
+
+
+sub SetRecipients {
+    my $self=shift;
+
+    push(@{$self->{'To'}}, @{$self->TicketObj->Requestors->Emails});
+    
+    return(1);
+}
+
+# }}}
+
+
+# {{{ sub SetReturnAddress 
+
+=head2 SetReturnAddress
+
+Set this message\'s return address to the apropriate queue address
+
+=cut
+
+sub SetReturnAddress {
+    my $self = shift;
+    my %args = ( is_comment => 0,
+                @_
+              );
+    
+    if ($args{'is_comment'}) { 
+       $replyto = $self->TicketObj->QueueObj->CommentAddress || 
+                    $RT::CommentAddress;
+    }
+    else {
+       $replyto = $self->TicketObj->QueueObj->CorrespondAddress ||
+                    $RT::CorrespondAddress;
+    }
+    
+    unless ($self->TemplateObj->MIMEObj->head->get('From')) {
+       my $friendly_name=$self->TicketObj->QueueObj->Name;
+       $self->SetHeader('From', "\"$friendly_name\" <$replyto>");
+    }
+    
+    unless ($self->TemplateObj->MIMEObj->head->get('Reply-To')) {
+       $self->SetHeader('Reply-To', "$replyto");
+    }
+    
+}
+  
+# }}}
+
+1;
diff --git a/rt/lib/RT/Action/Generic.pm b/rt/lib/RT/Action/Generic.pm
new file mode 100755 (executable)
index 0000000..ecfd4ab
--- /dev/null
@@ -0,0 +1,155 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Action/Generic.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-2000 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+
+=head1 NAME
+
+  RT::Action::Generic - a generic baseclass for RT Actions
+
+=head1 SYNOPSIS
+
+  use RT::Action::Generic;
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Action::Generic);
+
+=end testing
+
+=cut
+
+package RT::Action::Generic;
+
+# {{{ sub new 
+sub new  {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self  = {};
+  bless ($self, $class);
+  $self->_Init(@_);
+  return $self;
+}
+# }}}
+
+# {{{ sub _Init 
+sub _Init  {
+  my $self = shift;
+  my %args = ( TransactionObj => undef,
+              TicketObj => undef,
+              ScripObj => undef,
+              TemplateObj => undef,
+              Argument => undef,
+              Type => undef,
+              @_ );
+  
+  
+  $self->{'Argument'} = $args{'Argument'};
+  $self->{'ScripObj'} = $args{'ScripObj'};
+  $self->{'TicketObj'} = $args{'TicketObj'};
+  $self->{'TransactionObj'} = $args{'TransactionObj'};
+  $self->{'TemplateObj'} = $args{'TemplateObj'};
+  $self->{'Type'} = $args{'Type'};
+}
+# }}}
+
+# Access Scripwide data
+
+# {{{ sub Argument 
+sub Argument  {
+  my $self = shift;
+  return($self->{'Argument'});
+}
+# }}}
+
+# {{{ sub TicketObj
+sub TicketObj  {
+  my $self = shift;
+  return($self->{'TicketObj'});
+}
+# }}}
+
+# {{{ sub TransactionObj
+sub TransactionObj  {
+  my $self = shift;
+  return($self->{'TransactionObj'});
+}
+# }}}
+
+# {{{ sub TemplateObj
+sub TemplateObj  {
+  my $self = shift;
+  return($self->{'TemplateObj'});
+}
+# }}}
+
+# {{{ sub Type
+sub Type  {
+  my $self = shift;
+  return($self->{'Type'});
+}
+# }}}
+
+
+# Scrip methods
+
+#Do what we need to do and send it out.
+
+# {{{ sub Commit 
+sub Commit  {
+  my $self = shift;
+  return(0,"Commit Stubbed");
+}
+# }}}
+
+
+#What does this type of Action does
+
+# {{{ sub Describe 
+sub Describe  {
+  my $self = shift;
+  return ("No description for " . ref $self);
+}
+# }}}
+
+
+#Parse the templates, get things ready to go.
+
+# {{{ sub Prepare 
+sub Prepare  {
+  my $self = shift;
+  return (0,"Prepare Stubbed");
+}
+# }}}
+
+
+#If this rule applies to this transaction, return true.
+
+# {{{ sub IsApplicable 
+sub IsApplicable  {
+  my $self = shift;
+  return(undef);
+}
+# }}}
+
+# {{{ sub DESTROY
+sub DESTROY {
+    my $self = shift;
+
+    # We need to clean up all the references that might maybe get
+    # oddly circular
+    $self->{'TemplateObj'} =undef
+    $self->{'TicketObj'} = undef;
+    $self->{'TransactionObj'} = undef;
+    $self->{'ScripObj'} = undef;
+
+
+     
+}
+
+# }}}
+1;
diff --git a/rt/lib/RT/Action/Notify.pm b/rt/lib/RT/Action/Notify.pm
new file mode 100755 (executable)
index 0000000..6dca4fd
--- /dev/null
@@ -0,0 +1,99 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Action/Notify.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+package RT::Action::Notify;
+require RT::Action::SendEmail;
+@ISA = qw(RT::Action::SendEmail);
+
+# {{{ sub SetRecipients
+
+=head2 SetRecipients
+
+Sets the recipients of this meesage to Owner, Requestor, AdminCc, Cc or All. 
+Explicitly B<does not> notify the creator of the transaction.
+
+=cut
+
+sub SetRecipients {
+    my $self = shift;
+
+    $arg = $self->Argument;
+
+    $arg =~ s/\bAll\b/Owner,Requestor,AdminCc,Cc/;
+
+    my ( @To, @PseudoTo, @Cc, @Bcc );
+
+
+    if ($arg =~ /\bOtherRecipients\b/) {
+        if ($self->TransactionObj->Message->First) {
+            push (@Cc, $self->TransactionObj->Message->First->GetHeader('RT-Send-Cc'));
+            push (@Bcc, $self->TransactionObj->Message->First->GetHeader('RT-Send-Bcc'));
+        }
+    }
+
+    if ( $arg =~ /\bRequestor\b/ ) {
+        push ( @To, @{ $self->TicketObj->Requestors->Emails } );
+    }
+
+    
+
+    if ( $arg =~ /\bCc\b/ ) {
+
+        #If we have a To, make the Ccs, Ccs, otherwise, promote them to To
+        if (@To) {
+            push ( @Cc, @{ $self->TicketObj->Cc->Emails } );
+            push ( @Cc, @{ $self->TicketObj->QueueObj->Cc->Emails } );
+        }
+        else {
+            push ( @Cc, @{ $self->TicketObj->Cc->Emails } );
+            push ( @To, @{ $self->TicketObj->QueueObj->Cc->Emails } );
+        }
+    }
+
+    if ( ( $arg =~ /\bOwner\b/ )
+        && ( $self->TicketObj->OwnerObj->id != $RT::Nobody->id ) )
+    {
+
+        # If we're not sending to Ccs or requestors, 
+        # then the Owner can be the To.
+        if (@To) {
+            push ( @Bcc, $self->TicketObj->OwnerObj->EmailAddress );
+        }
+        else {
+            push ( @To, $self->TicketObj->OwnerObj->EmailAddress );
+        }
+
+    }
+
+    if ( $arg =~ /\bAdminCc\b/ ) {
+        push ( @Bcc, @{ $self->TicketObj->AdminCc->Emails } );
+        push ( @Bcc, @{ $self->TicketObj->QueueObj->AdminCc->Emails } );
+    }
+
+    if ($RT::UseFriendlyToLine) {
+        unless (@To) {
+            push ( @PseudoTo,
+                "\"$arg of $RT::rtname Ticket #"
+                  . $self->TicketObj->id . "\":;" );
+        }
+    }
+
+    my $creator = $self->TransactionObj->CreatorObj->EmailAddress();
+
+    #Strip the sender out of the To, Cc and AdminCc and set the 
+    # recipients fields used to build the message by the superclass.
+
+    $RT::Logger->debug("$self: To is ".join(",",@To));
+    $RT::Logger->debug("$self: Cc is ".join(",",@Cc));
+    $RT::Logger->debug("$self: Bcc is ".join(",",@Bcc));
+
+    @{ $self->{'To'} }  = grep ( !/^$creator$/, @To );
+    @{ $self->{'Cc'} }  = grep ( !/^$creator$/, @Cc );
+    @{ $self->{'Bcc'} } = grep ( !/^$creator$/, @Bcc );
+    @{ $self->{'PseudoTo'} } = @PseudoTo;
+    return (1);
+
+}
+
+# }}}
+
+1;
diff --git a/rt/lib/RT/Action/NotifyAsComment.pm b/rt/lib/RT/Action/NotifyAsComment.pm
new file mode 100755 (executable)
index 0000000..c72bfff
--- /dev/null
@@ -0,0 +1,25 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Action/NotifyAsComment.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+package RT::Action::NotifyAsComment;
+require RT::Action::Notify;
+@ISA = qw(RT::Action::Notify);
+
+
+=head2 SetReturnAddress
+
+Tell SendEmail that this message should come out as a comment. 
+Calls SUPER::SetReturnAddress.
+
+=cut
+
+sub SetReturnAddress {
+       my $self = shift;
+       
+       # Tell RT::Action::SendEmail that this should come 
+       # from the relevant comment email address.
+       $self->{'comment'} = 1;
+       
+       return($self->SUPER::SetReturnAddress(is_comment => 1));
+}
+1;
+
diff --git a/rt/lib/RT/Action/OpenDependent.pm b/rt/lib/RT/Action/OpenDependent.pm
new file mode 100644 (file)
index 0000000..b271e47
--- /dev/null
@@ -0,0 +1,55 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Action/Attic/OpenDependent.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# This Action will open the BASE if a dependent is resolved.
+
+package RT::Action::OpenDependent;
+require RT::Action::Generic;
+require RT::Links;
+@ISA=qw(RT::Action::Generic);
+
+#Do what we need to do and send it out.
+
+#What does this type of Action does
+
+# {{{ sub Describe 
+sub Describe  {
+  my $self = shift;
+  return (ref $self . " will stall a [local] BASE if it's open and a dependency link is created.");
+}
+# }}}
+
+
+# {{{ sub Prepare 
+sub Prepare  {
+    # nothing to prepare
+    return 1;
+}
+# }}}
+
+sub Commit {
+    my $self = shift;
+
+    my $Links=RT::Links->new($RT::SystemUser);
+    $Links->Limit(FIELD => 'Type', VALUE => 'DependsOn');
+    $Links->Limit(FIELD => 'Target', VALUE => $self->TicketObj->id);
+
+    while (my $Link=$Links->Next()) {
+       next unless $Link->BaseIsLocal;
+       my $base=RT::Ticket->new($self->TicketObj->CurrentUser);
+       # Todo: Only work if Base is a plain ticket num:
+       $base->Load($Link->Base);
+        $base->Open if $base->Status eq 'stalled';
+    }
+}
+
+
+# Applicability checked in Commit.
+
+# {{{ sub IsApplicable 
+sub IsApplicable  {
+  my $self = shift;
+  1;  
+  return 1;
+}
+# }}}
+
+1;
diff --git a/rt/lib/RT/Action/ResolveMembers.pm b/rt/lib/RT/Action/ResolveMembers.pm
new file mode 100644 (file)
index 0000000..00547eb
--- /dev/null
@@ -0,0 +1,57 @@
+# This Action will resolve all members of a resolved group ticket
+
+package RT::Action::ResolveMembers;
+require RT::Action::Generic;
+require RT::Links;
+@ISA=qw(RT::Action::Generic);
+
+#Do what we need to do and send it out.
+
+#What does this type of Action does
+
+# {{{ sub Describe 
+sub Describe  {
+  my $self = shift;
+  return (ref $self . " will resolve all members of a resolved group ticket.");
+}
+# }}}
+
+
+# {{{ sub Prepare 
+sub Prepare  {
+    # nothing to prepare
+    return 1;
+}
+# }}}
+
+sub Commit {
+    my $self = shift;
+
+    my $Links=RT::Links->new($RT::SystemUser);
+    $Links->Limit(FIELD => 'Type', VALUE => 'MemberOf');
+    $Links->Limit(FIELD => 'Target', VALUE => $self->TicketObj->id);
+
+    while (my $Link=$Links->Next()) {
+       # Todo: Try to deal with remote URIs as well
+       next unless $Link->BaseIsLocal;
+       my $base=RT::Ticket->new($self->TicketObj->CurrentUser);
+       # Todo: Only work if Base is a plain ticket num:
+       $base->Load($Link->Base);
+       # I'm afraid this might be a major bottleneck if ResolveGroupTicket is on.
+        $base->Resolve;
+    }
+}
+
+
+# Applicability checked in Commit.
+
+# {{{ sub IsApplicable 
+sub IsApplicable  {
+  my $self = shift;
+  1;  
+  return 1;
+}
+# }}}
+
+1;
+
diff --git a/rt/lib/RT/Action/SendEmail.pm b/rt/lib/RT/Action/SendEmail.pm
new file mode 100755 (executable)
index 0000000..e3abb11
--- /dev/null
@@ -0,0 +1,468 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Action/SendEmail.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Copyright 1996-2002  Jesse Vincent <jesse@bestpractical.com> 
+# Portions Copyright 2000 Tobias Brox <tobix@cpan.org>
+# Released under the terms of version 2 of the GNU Public License
+
+package RT::Action::SendEmail;
+require RT::Action::Generic;
+
+@ISA = qw(RT::Action::Generic);
+
+
+=head1 NAME
+
+  RT::Action::SendEmail - An Action which users can use to send mail 
+  or can subclassed for more specialized mail sending behavior. 
+  RT::Action::AutoReply is a good example subclass.
+
+
+=head1 SYNOPSIS
+
+  require RT::Action::SendEmail;
+  @ISA  = qw(RT::Action::SendEmail);
+
+
+=head1 DESCRIPTION
+
+Basically, you create another module RT::Action::YourAction which ISA
+RT::Action::SendEmail.
+
+If you want to set the recipients of the mail to something other than
+the addresses mentioned in the To, Cc, Bcc and headers in
+the template, you should subclass RT::Action::SendEmail and override
+either the SetRecipients method or the SetTo, SetCc, etc methods (see
+the comments for the SetRecipients sub).
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Action::SendEmail);
+
+=end testing
+
+
+=head1 AUTHOR
+
+Jesse Vincent <jesse@bestpractical.com> and Tobias Brox <tobix@cpan.org>
+
+=head1 SEE ALSO
+
+perl(1).
+
+=cut
+
+# {{{ Scrip methods (_Init, Commit, Prepare, IsApplicable)
+
+# {{{ sub _Init 
+# We use _Init from RT::Action
+# }}}
+
+# {{{ sub Commit 
+#Do what we need to do and send it out.
+sub Commit  {
+    my $self = shift;
+    #send the email
+    
+    # If there are no recipients, don't try to send the message.
+    # If the transaction has content and has the header RT-Squelch-Replies-To
+    
+    if (defined $self->TransactionObj->Message->First()) { 
+       my $headers = $self->TransactionObj->Message->First->Headers();
+       
+       if ($headers =~ /^RT-Squelch-Replies-To: (.*?)$/si) {
+           my @blacklist = split(/,/,$1);
+           
+           # Cycle through the people we're sending to and pull out anyone on the
+           # system blacklist
+           
+           foreach my $person_to_yank (@blacklist) {
+               $person_to_yank =~ s/\s//g;
+               @{$self->{'To'}} = grep (!/^$person_to_yank$/, @{$self->{'To'}});
+               @{$self->{'Cc'}} = grep (!/^$person_to_yank$/, @{$self->{'Cc'}});
+               @{$self->{'Bcc'}} = grep (!/^$person_to_yank$/, @{$self->{'Bcc'}});
+           }
+       }
+    }
+    
+    # Go add all the Tos, Ccs and Bccs that we need to to the message to 
+    # make it happy, but only if we actually have values in those arrays.
+    
+    $self->SetHeader('To', join(',', @{$self->{'To'}})) 
+      if (@{$self->{'To'}});
+    $self->SetHeader('Cc', join(',' , @{$self->{'Cc'}})) 
+      if (@{$self->{'Cc'}});
+       $self->SetHeader('Bcc', join(',', @{$self->{'Bcc'}})) 
+         if (@{$self->{'Bcc'}});;
+    
+    my $MIMEObj = $self->TemplateObj->MIMEObj;
+    
+
+    $MIMEObj->make_singlepart;
+    
+    
+    #If we don't have any recipients to send to, don't send a message;
+    unless ($MIMEObj->head->get('To') ||
+           $MIMEObj->head->get('Cc') || 
+           $MIMEObj->head->get('Bcc') ) {
+       $RT::Logger->debug("$self: No recipients found. Not sending.\n");
+       return(1);
+    }
+
+    # PseudoTo (fake to headers) shouldn't get matched for message recipients.
+    # If we don't have any 'To' header, drop in the pseudo-to header.
+
+    $self->SetHeader('To', join(',', @{$self->{'PseudoTo'}}))
+      if ( (@{$self->{'PseudoTo'}}) and (! $MIMEObj->head->get('To')));
+    
+    if ($RT::MailCommand eq 'sendmailpipe') {
+       open (MAIL, "|$RT::SendmailPath $RT::SendmailArguments") || return(0);
+       print MAIL $MIMEObj->as_string;
+       close(MAIL);
+    }
+    else {
+       unless ($MIMEObj->send($RT::MailCommand, $RT::MailParams)) {
+           $RT::Logger->crit("$self: Could not send mail for ".
+                             $self->TransactionObj . "\n");
+           return(0);
+       }
+    }
+    
+    return (1);
+    
+}
+# }}}
+
+# {{{ sub Prepare 
+
+sub Prepare  {
+  my $self = shift;
+  
+  # This actually populates the MIME::Entity fields in the Template Object
+  
+  unless ($self->TemplateObj) {
+    $RT::Logger->warning("No template object handed to $self\n");
+  }
+  
+  unless ($self->TransactionObj) {
+    $RT::Logger->warning("No transaction object handed to $self\n");
+    
+  }
+  
+  unless ($self->TicketObj) {
+    $RT::Logger->warning("No ticket object handed to $self\n");
+      
+  }
+  
+  
+  $self->TemplateObj->Parse(Argument => $self->Argument,
+                           TicketObj => $self->TicketObj, 
+                           TransactionObj => $self->TransactionObj);
+  
+  # Header
+  
+  $self->SetSubject();
+  
+  $self->SetSubjectToken();
+  
+  $self->SetRecipients();  
+  
+  $self->SetReturnAddress();
+
+  $self->SetRTSpecialHeaders();
+  
+  return 1;
+  
+}
+
+# }}}
+
+# }}}
+
+# {{{ Deal with message headers (Set* subs, designed for  easy overriding)
+
+# {{{ sub SetRTSpecialHeaders
+
+# This routine adds all the random headers that RT wants in a mail message
+# that don't matter much to anybody else.
+
+sub SetRTSpecialHeaders {
+    my $self = shift;
+    
+    $self->SetReferences();
+
+    $self->SetMessageID();
+    
+    $self->SetPrecedence();
+
+    $self->SetHeader('X-RT-Loop-Prevention', $RT::rtname); 
+    $self->SetHeader('RT-Ticket', $RT::rtname. " #".$self->TicketObj->id());
+    $self->SetHeader
+      ('Managed-by',"RT $RT::VERSION (http://bestpractical.com/rt/)");
+    
+    $self->SetHeader('RT-Originator', $self->TransactionObj->CreatorObj->EmailAddress);
+    return();
+    
+}
+
+
+
+# {{{ sub SetReferences
+
+=head2 SetReferences 
+  
+  # This routine will set the References: and In-Reply-To headers,
+# autopopulating it with all the correspondence on this ticket so
+# far. This should make RT responses threadable.
+
+=cut
+
+sub SetReferences {
+  my $self = shift;
+  
+  # TODO: this one is broken.  What is this email really a reply to?
+  # If it's a reply to an incoming message, we'll need to use the
+  # actual message-id from the appropriate Attachment object.  For
+  # incoming mails, we would like to preserve the In-Reply-To and/or
+  # References.
+
+  $self->SetHeader
+    ('In-Reply-To', "<rt-".$self->TicketObj->id().
+     "\@".$RT::rtname.">");
+
+
+  # TODO We should always add References headers for all message-ids
+  # of previous messages related to this ticket.
+}
+
+# }}}
+
+# {{{ sub SetMessageID
+
+# Without this one, threading won't work very nice in email agents.
+# Anyway, I'm not really sure it's that healthy if we need to send
+# several separate/different emails about the same transaction.
+
+sub SetMessageID {
+  my $self = shift;
+
+  # TODO this one might be sort of broken.  If we have several scrips +++
+  # sending several emails to several different persons, we need to
+  # pull out different message-ids.  I'd suggest message ids like
+  # "rt-ticket#-transaction#-scrip#-receipient#"
+
+  $self->SetHeader
+    ('Message-ID', "<rt-".$self->TicketObj->id().
+     "-".
+     $self->TransactionObj->id()."." .rand(20) . "\@".$RT::Organization.">")
+      unless $self->TemplateObj->MIMEObj->head->get('Message-ID');
+}
+
+
+# }}}
+
+# }}}
+
+# {{{ sub SetReturnAddress 
+
+sub SetReturnAddress {
+
+  my $self = shift;
+  my %args = ( is_comment => 0,
+              @_ );
+
+  # From and Reply-To
+  # $args{is_comment} should be set if the comment address is to be used.
+  my $replyto;
+
+  if ($args{'is_comment'}) { 
+      $replyto = $self->TicketObj->QueueObj->CommentAddress || 
+                 $RT::CommentAddress;
+  }
+  else {
+      $replyto = $self->TicketObj->QueueObj->CorrespondAddress ||
+                 $RT::CorrespondAddress;
+  }
+    
+  unless ($self->TemplateObj->MIMEObj->head->get('From')) {
+      my $friendly_name=$self->TransactionObj->CreatorObj->RealName;
+
+      if ($friendly_name =~ /^\S+\@\S+$/) { # A "bare" mail address
+          $friendly_name =~ s/"/\\"/g;
+          $friendly_name = qq|"$friendly_name"|;
+      }
+
+
+      # TODO: this "via RT" should really be site-configurable.
+      $self->SetHeader('From', "\"$friendly_name via RT\" <$replyto>");
+  }
+  
+  unless ($self->TemplateObj->MIMEObj->head->get('Reply-To')) {
+      $self->SetHeader('Reply-To', "$replyto");
+  }
+  
+}
+
+# }}}
+
+# {{{ sub SetHeader
+
+sub SetHeader {
+  my $self = shift;
+  my $field = shift;
+  my $val = shift;
+
+  chomp $val;                                                                  
+  chomp $field;                                                                
+  $self->TemplateObj->MIMEObj->head->fold_length($field,10000);     
+  $self->TemplateObj->MIMEObj->head->add($field, $val);
+  return $self->TemplateObj->MIMEObj->head->get($field);
+}
+
+# }}}
+
+# {{{ sub SetRecipients
+
+=head2 SetRecipients
+
+Dummy method to be overriden by subclasses which want to set the recipients.
+
+=cut
+
+sub SetRecipients {
+    my $self = shift;
+    return();
+}
+
+# }}}
+
+# {{{ sub SetTo
+
+sub SetTo {
+    my $self=shift;
+    my $addresses = shift;
+    return $self->SetHeader('To',$addresses);
+}
+# }}}
+
+# {{{ sub SetCc
+=head2 SetCc
+
+Takes a string that is the addresses you want to Cc
+
+=cut
+
+sub SetCc {
+    my $self=shift;
+    my $addresses = shift;
+
+    return $self->SetHeader('Cc', $addresses);
+}
+# }}}
+
+# {{{ sub SetBcc
+
+=head2 SetBcc
+
+Takes a string that is the addresses you want to Bcc
+
+=cut
+sub SetBcc {
+    my $self=shift;
+    my $addresses = shift;
+
+    return $self->SetHeader('Bcc', $addresses);
+}
+
+# }}}
+
+# {{{ sub SetPrecedence 
+
+sub SetPrecedence {
+  my $self = shift;
+
+  unless ($self->TemplateObj->MIMEObj->head->get("Precedence")) { 
+    $self->SetHeader('Precedence', "bulk");
+   }
+}
+
+# }}}
+
+# {{{ sub SetSubject
+
+=head2 SetSubject
+
+This routine sets the subject. it does not add the rt tag. that gets done elsewhere
+If $self->{'Subject'} is already defined, it uses that. otherwise, it tries to get
+the transaction's subject.
+
+=cut 
+
+sub SetSubject {
+  my $self = shift;
+  unless ($self->TemplateObj->MIMEObj->head->get('Subject')) {
+    my $message=$self->TransactionObj->Message;
+    my $ticket=$self->TicketObj->Id;
+    
+    my $subject;
+    
+    if ($self->{'Subject'}) {
+      $subject = $self->{'Subject'};
+    }
+    elsif (($message->First()) &&
+          ($message->First->Headers)) {
+      $header = $message->First->Headers();
+      $header =~ s/\n\s+/ /g; 
+      if ( $header =~ /^Subject: (.*?)$/m ) {
+       $subject = $1;
+      }
+      else {
+       $subject = $self->TicketObj->Subject();
+      }
+      
+    }
+    else {
+      $subject = $self->TicketObj->Subject();
+    }
+    
+    $subject =~ s/(\r\n|\n|\s)/ /gi;
+
+    chomp $subject;
+    $self->SetHeader('Subject',$subject);
+    
+    }
+  return($subject);
+}
+# }}}
+
+# {{{ sub SetSubjectToken
+
+=head2 SetSubjectToken
+
+ This routine fixes the RT tag in the subject. It's unlikely that you want to overwrite this.
+
+=cut
+
+sub SetSubjectToken {
+  my $self=shift;
+  my $tag = "[$RT::rtname #".$self->TicketObj->id."]";
+  my $sub = $self->TemplateObj->MIMEObj->head->get('Subject');
+  unless ($sub =~ /\Q$tag\E/) {
+    $sub =~ s/(\r\n|\n|\s)/ /gi;
+    chomp $sub;
+    $self->TemplateObj->MIMEObj->head->replace('Subject', "$tag $sub");
+  }
+}
+
+# }}}
+
+# }}}
+
+__END__
+
+# {{{ POD
+
+# }}}
+
+1;
+
diff --git a/rt/lib/RT/Action/SendPasswordEmail.pm b/rt/lib/RT/Action/SendPasswordEmail.pm
new file mode 100755 (executable)
index 0000000..91bb3c1
--- /dev/null
@@ -0,0 +1,170 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Action/Attic/SendPasswordEmail.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Copyright 2001  Jesse Vincent <jesse@fsck.com>
+# Released under the terms of the GNU Public License
+
+package RT::Action::SendPasswordEmail;
+require RT::Action::Generic;
+
+@ISA = qw(RT::Action::Generic);
+
+
+=head1 NAME
+
+  RT::Action::SendGenericEmail - An Action which users can use to send mail 
+  or can subclassed for more specialized mail sending behavior. 
+
+
+
+=head1 SYNOPSIS
+
+  require RT::Action::SendPasswordEmail;
+
+
+=head1 DESCRIPTION
+
+Basically, you create another module RT::Action::YourAction which ISA
+RT::Action::SendEmail.
+
+If you want to set the recipients of the mail to something other than
+the addresses mentioned in the To, Cc, Bcc and headers in
+the template, you should subclass RT::Action::SendEmail and override
+either the SetRecipients method or the SetTo, SetCc, etc methods (see
+the comments for the SetRecipients sub).
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Action::SendPasswordEmail);
+
+=end testing
+
+
+=head1 AUTHOR
+
+Jesse Vincent <jesse@bestpractical.com>
+
+=head1 SEE ALSO
+
+perl(1).
+
+=cut
+
+# {{{ Scrip methods (_Init, Commit, Prepare, IsApplicable)
+
+# {{{ sub Commit 
+
+#Do what we need to do and send it out.
+
+sub Commit  {
+    my $self = shift;
+    #send the email
+    
+    
+    
+
+    
+    my $MIMEObj = $self->TemplateObj->MIMEObj;
+    
+    
+    $MIMEObj->make_singlepart;
+    
+    #If we don\'t have any recipients to send to, don\'t send a message;
+    unless ($MIMEObj->head->get('To')) {
+       $RT::Logger->debug("$self: No recipients found. Not sending.\n");
+       return(1);
+    }
+    
+    if ($RT::MailCommand eq 'sendmailpipe') {
+       open (MAIL, "|$RT::SendmailPath $RT::SendmailArguments") || return(0);
+       print MAIL $MIMEObj->as_string;
+       close(MAIL);
+    }
+    else {
+       unless ($MIMEObj->send($RT::MailCommand, $RT::MailParams)) {
+           $RT::Logger->crit("$self: Could not send mail for ".
+                             $self->TransactionObj . "\n");
+           return(0);
+       }
+    }
+    
+    return (1);
+    
+}
+# }}}
+
+# {{{ sub Prepare 
+
+sub Prepare  {
+    my $self = shift;
+    
+    # This actually populates the MIME::Entity fields in the Template Object
+    
+    unless ($self->TemplateObj) {
+       $RT::Logger->warning("No template object handed to $self\n");
+    }
+    
+    
+    unless ($self->TemplateObj->MIMEObj->head->get('Reply-To')) {
+       $self->SetHeader('Reply-To',$RT::CorrespondAddress );
+    }
+
+    
+    $self->SetHeader('Precedence', "bulk");
+    $self->SetHeader('X-RT-Loop-Prevention', $RT::rtname); 
+    $self->SetHeader
+      ('Managed-by',"Request Tracker $RT::VERSION (http://www.fsck.com/projects/rt/)");
+    
+    $self->TemplateObj->Parse(Argument => $self->Argument);
+    
+    
+    return 1;
+}
+
+# }}}
+
+# }}}
+
+
+# {{{ sub SetTo
+
+=head2 SetTo EMAIL
+
+Sets this message's "To" field to EMAIL
+
+=cut
+
+sub SetTo {
+    my $self = shift;
+    my $to = shift;
+    $self->SetHeader('To',$to);
+}
+
+# }}}
+
+# {{{ sub SetHeader
+
+sub SetHeader {
+  my $self = shift;
+  my $field = shift;
+  my $val = shift;
+
+  chomp $val;                                                                  
+  chomp $field;                                                                
+  $self->TemplateObj->MIMEObj->head->fold_length($field,10000);     
+  $self->TemplateObj->MIMEObj->head->add($field, $val);
+  return $self->TemplateObj->MIMEObj->head->get($field);
+}
+
+# }}}
+
+
+
+__END__
+
+# {{{ POD
+
+# }}}
+
+1;
+
diff --git a/rt/lib/RT/Action/StallDependent.pm b/rt/lib/RT/Action/StallDependent.pm
new file mode 100644 (file)
index 0000000..09d3448
--- /dev/null
@@ -0,0 +1,68 @@
+# This Action will stall the BASE if a dependency or membership link
+# (according to argument) is created and if BASE is open.
+
+# TODO: Rename this .pm
+
+package RT::Action::StallDependent;
+require RT::Action::Generic;
+@ISA=qw|RT::Action::Generic|;
+
+# {{{ sub Describe 
+sub Describe  {
+  my $self = shift;
+  return (ref $self . " will stall a [local] BASE if it's dependent [or member] of a linked up request.");
+}
+# }}}
+
+
+# {{{ sub Prepare 
+sub Prepare  {
+    # nothing to prepare
+    return 1;
+}
+# }}}
+
+sub Commit {
+    my $self = shift;
+    # Find all Dependent
+    my $arg=$self->Argument || "DependsOn";
+    unless ($self->TransactionObj->Data =~ /^([^ ]+) $arg /) {
+       warn; return 0;
+    }
+    my $base_id=$1;
+    my $base;
+    if ($1 eq "THIS") {
+       $base=$self->TicketObj;
+    } else {
+       $base_id=&RT::Link::_IsLocal(undef, $base_id) || return 0;
+       $base=RT::Ticket->new($self->TicketObj->CurrentUser);
+       $base->Load($base_id);
+    }
+    $base->Stall if $base->Status eq 'open';
+    return 0;
+}
+
+
+# {{{ sub IsApplicable 
+
+# Only applicable if:
+# 1. the link action is a dependency
+# 2. BASE is a local ticket
+
+sub IsApplicable  {
+  my $self = shift;
+
+  my $arg=$self->Argument || "DependsOn";
+
+  # 1:
+  $self->TransactionObj->Data =~ /^([^ ]*) $arg / || return 0;
+
+  # 2:
+  # (dirty!)
+  &RT::Link::_IsLocal(undef,$1) || return 0;
+
+  return 1;
+}
+# }}}
+
+1;
diff --git a/rt/lib/RT/Attachment.pm b/rt/lib/RT/Attachment.pm
new file mode 100755 (executable)
index 0000000..916ac35
--- /dev/null
@@ -0,0 +1,423 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attachment.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Copyright 2000 Jesse Vincent <jesse@fsck.com>
+# Released under the terms of the GNU Public License
+
+=head1 NAME
+
+  RT::Attachment -- an RT attachment object
+
+=head1 SYNOPSIS
+
+  use RT::Attachment;
+
+
+=head1 DESCRIPTION
+
+This module should never be instantiated directly by client code. it's an internal 
+module which should only be instantiated through exported APIs in Ticket, Queue and other 
+similar objects.
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Attachment);
+
+=end testing
+
+=cut
+
+package RT::Attachment;
+use RT::Record;
+use MIME::Base64;
+use vars qw|@ISA|;
+@ISA= qw(RT::Record);
+
+# {{{ sub _Init
+sub _Init  {
+    my $self = shift; 
+    $self->{'table'} = "Attachments";
+    return($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _ClassAccessible 
+sub _ClassAccessible {
+    {
+    TransactionId   => { 'read'=>1, 'public'=>1, },
+    MessageId       => { 'read'=>1, },
+    Parent          => { 'read'=>1, },
+    ContentType     => { 'read'=>1, },
+    Subject         => { 'read'=>1, },
+    Content         => { 'read'=>1, },
+    ContentEncoding => { 'read'=>1, },
+    Headers         => { 'read'=>1, },
+    Filename        => { 'read'=>1, },
+    Creator         => { 'read'=>1, 'auto'=>1, },
+    Created         => { 'read'=>1, 'auto'=>1, },
+  };
+}
+# }}}
+
+# {{{ sub TransactionObj 
+
+=head2 TransactionObj
+
+Returns the transaction object asscoiated with this attachment.
+
+=cut
+
+sub TransactionObj {
+    require RT::Transaction;
+    my $self=shift;
+    unless (exists $self->{_TransactionObj}) {
+       $self->{_TransactionObj}=RT::Transaction->new($self->CurrentUser);
+       $self->{_TransactionObj}->Load($self->TransactionId);
+    }
+    return $self->{_TransactionObj};
+}
+
+# }}}
+
+# {{{ sub Create 
+
+=head2 Create
+
+Create a new attachment. Takes a paramhash:
+    
+    'Attachment' Should be a single MIME body with optional subparts
+    'Parent' is an optional Parent RT::Attachment object
+    'TransactionId' is the mandatory id of the Transaction this attachment is associated with.;
+
+=cut
+
+sub Create  {
+    my $self = shift;
+    my ($id);
+    my %args = ( id => 0,
+                TransactionId => 0,
+                Parent => 0,
+                Attachment => undef,
+                @_
+              );
+    
+    
+    #For ease of reference
+    my $Attachment = $args{'Attachment'};
+    
+    #if we didn't specify a ticket, we need to bail
+    if ( $args{'TransactionId'} == 0) {
+       $RT::Logger->crit("RT::Attachment->Create couldn't, as you didn't specify a transaction\n");
+       return (0);
+       
+    }
+    
+    #If we possibly can, collapse it to a singlepart
+    $Attachment->make_singlepart;
+    
+    #Get the subject
+    my $Subject = $Attachment->head->get('subject',0);
+    defined($Subject) or $Subject = '';
+    chomp($Subject);
+  
+    #Get the filename
+    my $Filename = $Attachment->head->recommended_filename;
+    
+    if ($Attachment->parts) {
+       $id = $self->SUPER::Create(TransactionId => $args{'TransactionId'},
+                                  Parent => 0,
+                                  ContentType  => $Attachment->mime_type,
+                                  Headers => $Attachment->head->as_string,
+                                  Subject => $Subject,
+                                  
+                                 );
+       foreach my $part ($Attachment->parts) { 
+           my $SubAttachment = new RT::Attachment($self->CurrentUser);
+           $SubAttachment->Create(TransactionId => $args{'TransactionId'},
+                                  Parent => $id,
+                                  Attachment => $part,
+                                  ContentType  => $Attachment->mime_type,
+                                  Headers => $Attachment->head->as_string(),
+                                  
+                                 );
+       }
+       return ($id);
+    }
+  
+  
+    #If it's not multipart
+    else {
+       
+       my $ContentEncoding = 'none'; 
+       
+       my $Body = $Attachment->bodyhandle->as_string;
+       
+       #get the max attachment length from RT
+       my $MaxSize = $RT::MaxAttachmentSize;
+       
+       #if the current attachment contains nulls and the 
+       #database doesn't support embedded nulls
+       
+       if ( (! $RT::Handle->BinarySafeBLOBs) &&
+            ( $Body =~ /\x00/ ) ) {
+           # set a flag telling us to mimencode the attachment
+           $ContentEncoding = 'base64';
+           
+           #cut the max attchment size by 25% (for mime-encoding overhead.
+           $RT::Logger->debug("Max size is $MaxSize\n");
+           $MaxSize = $MaxSize * 3/4;  
+       }
+       
+       #if the attachment is larger than the maximum size
+       if (($MaxSize) and ($MaxSize < length($Body))) {
+           # if we're supposed to truncate large attachments
+           if ($RT::TruncateLongAttachments) {
+               # truncate the attachment to that length.
+               $Body = substr ($Body, 0, $MaxSize);
+
+           }
+           
+           # elsif we're supposed to drop large attachments on the floor,
+           elsif ($RT::DropLongAttachments) {
+               # drop the attachment on the floor
+               $RT::Logger->info("$self: Dropped an attachment of size ". length($Body).
+                                 "\n". "It started: ". substr($Body, 0, 60) . "\n");
+               return(undef);
+           }
+       }
+       # if we need to mimencode the attachment
+       if ($ContentEncoding eq 'base64') {
+           # base64 encode the attachment
+           $Body = MIME::Base64::encode_base64($Body);
+           
+       }
+       
+       my $id = $self->SUPER::Create(TransactionId => $args{'TransactionId'},
+                                     ContentType  => $Attachment->mime_type,
+                                     ContentEncoding => $ContentEncoding,
+                                     Parent => $args{'Parent'},
+                                     Content => $Body,
+                                     Headers => $Attachment->head->as_string,
+                                     Subject => $Subject,
+                                     Filename => $Filename,
+                                    );
+       return ($id);
+    }
+}
+
+# }}}
+
+
+# {{{ sub Content
+
+=head2 Content
+
+Returns the attachment's content. if it's base64 encoded, decode it 
+before returning it.
+
+=cut
+
+sub Content {
+  my $self = shift;
+  if ( $self->ContentEncoding eq 'none' || ! $self->ContentEncoding ) {
+      return $self->_Value('Content');
+  } elsif ( $self->ContentEncoding eq 'base64' ) {
+      return MIME::Base64::decode_base64($self->_Value('Content'));
+  } else {
+      return( "Unknown ContentEncoding ". $self->ContentEncoding);
+  }
+}
+
+
+# }}}
+
+# {{{ sub Children
+
+=head2 Children
+
+  Returns an RT::Attachments object which is preloaded with all Attachments objects with this Attachment\'s Id as their 'Parent'
+
+=cut
+
+sub Children {
+    my $self = shift;
+    
+    my $kids = new RT::Attachments($self->CurrentUser);
+    $kids->ChildrenOf($self->Id);
+    return($kids);
+}
+
+# }}}
+
+# {{{ UTILITIES
+
+# {{{ sub Quote 
+
+
+
+sub Quote {
+    my $self=shift;
+    my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
+             @_);
+
+    my ($quoted_content, $body, $headers);
+    my $max=0;
+
+    # TODO: Handle Multipart/Mixed (eventually fix the link in the
+    # ShowHistory web template?)
+    if ($self->ContentType =~ m{^(text/plain|message)}i) {
+       $body=$self->Content;
+
+       # Do we need any preformatting (wrapping, that is) of the message?
+
+       # Remove quoted signature.
+       $body =~ s/\n-- \n(.*)$//s;
+
+       # What's the longest line like?
+       foreach (split (/\n/,$body)) {
+           $max=length if ( length > $max);
+       }
+
+       if ($max>76) {
+           require Text::Wrapper;
+           my $wrapper=new Text::Wrapper
+               (
+                columns => 70, 
+                body_start => ($max > 70*3 ? '   ' : ''),
+                par_start => ''
+                );
+           $body=$wrapper->wrap($body);
+       }
+
+       $body =~ s/^/> /gm;
+
+       $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
+                   . "]:\n\n"
+               . $body . "\n\n";
+
+    } else {
+       $body = "[Non-text message not quoted]\n\n";
+    }
+    
+    $max=60 if $max<60;
+    $max=70 if $max>78;
+    $max+=2;
+
+    return (\$body, $max);
+}
+# }}}
+
+# {{{ sub NiceHeaders - pulls out only the most relevant headers
+
+=head2 NiceHeaders
+
+Returns the To, From, Cc, Date and Subject headers.
+
+It is a known issue that this breaks if any of these headers are not
+properly unfolded.
+
+=cut
+
+sub NiceHeaders {
+    my $self=shift;
+    my $hdrs="";
+    for (split(/\n/,$self->Headers)) {
+           $hdrs.="$_\n" if /^(To|From|RT-Send-Cc|Cc|Date|Subject): /i
+    }
+    return $hdrs;
+}
+# }}}
+
+# {{{ sub Headers
+
+=head2 Headers
+
+Returns this object's headers as a string.  This method specifically
+removes the RT-Send-Bcc: header, so as to never reveal to whom RT sent a Bcc.
+We need to record the RT-Send-Cc and RT-Send-Bcc values so that we can actually send
+out mail. (The mailing rules are seperated from the ticket update code by
+an abstraction barrier that makes it impossible to pass this data directly
+
+=cut
+
+sub Headers {
+    my $self = shift;
+    my $hdrs="";
+    for (split(/\n/,$self->SUPER::Headers)) {
+           $hdrs.="$_\n" unless /^(RT-Send-Bcc): /i
+    }
+    return $hdrs;
+}
+
+
+# }}}
+
+# {{{ sub GetHeader
+
+=head2 GetHeader ( 'Tag')
+
+Returns the value of the header Tag as a string. This bypasses the weeding out
+done in Headers() above.
+
+=cut
+
+sub GetHeader {
+    my $self = shift;
+    my $tag = shift;
+    foreach my $line (split(/\n/,$self->SUPER::Headers)) {
+        $RT::Logger->debug( "Does $line match $tag\n");
+        if ($line =~ /^$tag:\s+(.*)$/i) { #if we find the header, return its value
+            return ($1);
+        }
+    }
+    
+    # we found no header. return an empty string
+    return undef;
+}
+# }}}
+
+# {{{ sub _Value 
+
+=head2 _Value
+
+Takes the name of a table column.
+Returns its value as a string, if the user passes an ACL check
+
+=cut
+
+sub _Value  {
+
+    my $self = shift;
+    my $field = shift;
+    
+    
+    #if the field is public, return it.
+    if ($self->_Accessible($field, 'public')) {
+       #$RT::Logger->debug("Skipping ACL check for $field\n");
+       return($self->__Value($field));
+       
+    }
+    
+    #If it's a comment, we need to be extra special careful
+    elsif ( (($self->TransactionObj->CurrentUserHasRight('ShowTicketComments')) and
+            ($self->TransactionObj->Type eq 'Comment') )  or
+           ($self->TransactionObj->CurrentUserHasRight('ShowTicket'))) {
+       
+       return($self->__Value($field));
+    }
+    #if they ain't got rights to see, don't let em
+    else {
+           return(undef);
+       }
+       
+    
+}
+
+# }}}
+
+# }}}
+
+1;
diff --git a/rt/lib/RT/Attachments.pm b/rt/lib/RT/Attachments.pm
new file mode 100755 (executable)
index 0000000..73cd350
--- /dev/null
@@ -0,0 +1,99 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attachments.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Attachments - a collection of RT::Attachment objects
+
+=head1 SYNOPSIS
+
+  use RT::Attachments;
+
+=head1 DESCRIPTION
+
+This module should never be called directly by client code. it's an internal module which
+should only be accessed through exported APIs in Ticket, Queue and other similar objects.
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Attachments);
+
+=end testing
+
+=cut
+
+package RT::Attachments;
+
+use RT::EasySearch;
+
+@ISA= qw(RT::EasySearch);
+
+# {{{ sub _Init  
+sub _Init   {
+  my $self = shift;
+  $self->{'table'} = "Attachments";
+  $self->{'primary_key'} = "id";
+  return ( $self->SUPER::_Init(@_));
+}
+# }}}
+
+
+# {{{ sub ContentType
+
+=head2 ContentType (VALUE => 'text/plain', ENTRYAGGREGATOR => 'OR', OPERATOR => '=' ) 
+
+Limit result set to attachments of ContentType 'TYPE'...
+
+=cut
+
+
+sub ContentType  {
+  my $self = shift;
+  my %args = ( VALUE => 'text/plain',
+              OPERATOR => '=',
+              ENTRYAGGREGATOR => 'OR',
+              @_);
+
+  $self->Limit ( FIELD => 'ContentType',
+                VALUE => $args{'VALUE'},
+                OPERATOR => $args{'OPERATOR'},
+                ENTRYAGGREGATOR => $args{'ENTRYAGGREGATOR'});
+}
+# }}}
+
+# {{{ sub ChildrenOf 
+
+=head2 ChildrenOf ID
+
+Limit result set to children of Attachment ID
+
+=cut
+
+
+sub ChildrenOf  {
+  my $self = shift;
+  my $attachment = shift;
+  $self->Limit ( FIELD => 'Parent',
+                VALUE => $attachment);
+}
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem  {
+  my $self = shift;
+
+  use RT::Attachment;
+  my $item = new RT::Attachment($self->CurrentUser);
+  return($item);
+}
+# }}}
+  1;
+
+
+
+
diff --git a/rt/lib/RT/Condition/AnyTransaction.pm b/rt/lib/RT/Condition/AnyTransaction.pm
new file mode 100644 (file)
index 0000000..83e5de6
--- /dev/null
@@ -0,0 +1,23 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Condition/AnyTransaction.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Copyright 1996-2001 Jesse Vincent <jesse@fsck.com> 
+# Released under the terms of the GNU General Public License
+
+package RT::Condition::AnyTransaction;
+require RT::Condition::Generic;
+
+@ISA = qw(RT::Condition::Generic);
+
+
+=head2 IsApplicable
+
+This happens on every transaction. it's always applicable
+
+=cut
+
+sub IsApplicable {
+    my $self = shift;
+    return(1);
+}
+
+1;
+
diff --git a/rt/lib/RT/Condition/Generic.pm b/rt/lib/RT/Condition/Generic.pm
new file mode 100755 (executable)
index 0000000..393f4b7
--- /dev/null
@@ -0,0 +1,170 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Condition/Generic.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-2000 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+
+=head1 NAME
+
+  RT::Condition::Generic - ;
+
+=head1 SYNOPSIS
+
+    use RT::Condition::Generic;
+    my $foo = new RT::Condition::IsApplicable( 
+               TransactionObj => $tr, 
+               TicketObj => $ti, 
+               ScripObj => $scr, 
+               Argument => $arg, 
+               Type => $type);
+
+    if ($foo->IsApplicable) {
+          # do something
+    }
+
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Condition::Generic);
+
+=end testing
+
+
+=cut
+
+package RT::Condition::Generic;
+
+# {{{ sub new 
+sub new  {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self  = {};
+  bless ($self, $class);
+  $self->_Init(@_);
+  return $self;
+}
+# }}}
+
+# {{{ sub _Init 
+sub _Init  {
+  my $self = shift;
+  my %args = ( TransactionObj => undef,
+              TicketObj => undef,
+              ScripObj => undef,
+              TemplateObj => undef,
+              Argument => undef,
+              ApplicableTransTypes => undef,
+              @_ );
+  
+  
+  $self->{'Argument'} = $args{'Argument'};
+  $self->{'ScripObj'} = $args{'ScripObj'};
+  $self->{'TicketObj'} = $args{'TicketObj'};
+  $self->{'TransactionObj'} = $args{'TransactionObj'};
+  $self->{'ApplicableTransTypes'} = $args{'ApplicableTransTypes'};
+}
+# }}}
+
+# Access Scripwide data
+
+# {{{ sub Argument 
+
+=head2 Argument
+
+Return the optional argument associated with this ScripCondition
+
+=cut
+
+sub Argument  {
+  my $self = shift;
+  return($self->{'Argument'});
+}
+# }}}
+
+# {{{ sub TicketObj
+
+=head2 TicketObj
+
+Return the ticket object we're talking about
+
+=cut
+
+sub TicketObj  {
+  my $self = shift;
+  return($self->{'TicketObj'});
+}
+# }}}
+
+# {{{ sub TransactionObj
+
+=head2 TransactionObj
+
+Return the transaction object we're talking about
+
+=cut
+
+sub TransactionObj  {
+  my $self = shift;
+  return($self->{'TransactionObj'});
+}
+# }}}
+
+# {{{ sub Type
+
+=head2 Type 
+
+
+
+=cut
+
+sub ApplicableTransTypes  {
+  my $self = shift;
+  return($self->{'ApplicableTransTypes'});
+}
+# }}}
+
+
+# Scrip methods
+
+
+#What does this type of Action does
+
+# {{{ sub Describe 
+sub Describe  {
+  my $self = shift;
+  return ("No description for " . ref $self);
+}
+# }}}
+
+
+#Parse the templates, get things ready to go.
+
+#If this rule applies to this transaction, return true.
+
+# {{{ sub IsApplicable 
+sub IsApplicable  {
+  my $self = shift;
+  return(undef);
+}
+# }}}
+
+# {{{ sub DESTROY
+sub DESTROY {
+    my $self = shift;
+
+    # We need to clean up all the references that might maybe get
+    # oddly circular
+    $self->{'TemplateObj'} =undef
+    $self->{'TicketObj'} = undef;
+    $self->{'TransactionObj'} = undef;
+    $self->{'ScripObj'} = undef;
+     
+}
+
+# }}}
+1;
diff --git a/rt/lib/RT/Condition/NewDependency.pm b/rt/lib/RT/Condition/NewDependency.pm
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/rt/lib/RT/Condition/StatusChange.pm b/rt/lib/RT/Condition/StatusChange.pm
new file mode 100644 (file)
index 0000000..56419b2
--- /dev/null
@@ -0,0 +1,30 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Condition/StatusChange.pm,v 1.1 2002-08-12 06:17:08 ivan Exp $
+# Copyright 1996-2001 Jesse Vincent <jesse@fsck.com> 
+# Released under the terms of the GNU General Public License
+
+package RT::Condition::StatusChange;
+require RT::Condition::Generic;
+
+@ISA = qw(RT::Condition::Generic);
+
+
+=head2 IsApplicable
+
+If the argument passed in is equivalent to the new value of
+the Status Obj
+
+=cut
+
+sub IsApplicable {
+    my $self = shift;
+    if (($self->TransactionObj->Field eq 'Status') and 
+    ($self->Argument eq $self->TransactionObj->NewValue())) {
+       return(1);
+    } 
+    else {
+       return(undef);
+    }
+}
+
+1;
+
diff --git a/rt/lib/RT/CurrentUser.pm b/rt/lib/RT/CurrentUser.pm
new file mode 100755 (executable)
index 0000000..6997ddb
--- /dev/null
@@ -0,0 +1,270 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/CurrentUser.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-1999 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+
+=head1 NAME
+
+  RT::CurrentUser - an RT object representing the current user
+
+=head1 SYNOPSIS
+
+  use RT::CurrentUser
+
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::CurrentUser);
+
+=end testing
+
+=cut
+
+
+package RT::CurrentUser;
+use RT::Record;
+@ISA= qw(RT::Record);
+
+
+# {{{ sub _Init 
+
+#The basic idea here is that $self->CurrentUser is always supposed
+# to be a CurrentUser object. but that's hard to do when we're trying to load
+# the CurrentUser object
+
+sub _Init  {
+  my $self = shift;
+  my $Name = shift;
+
+  $self->{'table'} = "Users";
+
+  if (defined($Name)) {
+    $self->Load($Name);
+  }
+  
+  $self->_MyCurrentUser($self);
+
+}
+# }}}
+
+# {{{ sub Create
+
+sub Create {
+    return (0, 'Permission Denied');
+}
+
+# }}}
+
+# {{{ sub Delete
+
+sub Delete {
+    return (0, 'Permission Denied');
+}
+
+# }}}
+
+# {{{ sub UserObj
+
+=head2 UserObj
+
+  Returns the RT::User object associated with this CurrentUser object.
+
+=cut
+
+sub UserObj {
+    my $self = shift;
+    
+    unless ($self->{'UserObj'}) {
+       use RT::User;
+       $self->{'UserObj'} = RT::User->new($self);
+       unless ($self->{'UserObj'}->Load($self->Id)) {
+           $RT::Logger->err("Couldn't load ".$self->Id. "from the users database.\n");
+       }
+       
+    }
+    return ($self->{'UserObj'});
+}
+# }}}
+
+# {{{ sub _Accessible 
+sub _Accessible  {
+  my $self = shift;
+  my %Cols = (
+             Name => 'read',
+             Gecos => 'read',
+             RealName => 'read',
+             Password => 'neither',
+             EmailAddress => 'read',
+             Privileged => 'read',
+             IsAdministrator => 'read'
+            );
+  return($self->SUPER::_Accessible(@_, %Cols));
+}
+# }}}
+
+# {{{ sub LoadByEmail
+
+=head2 LoadByEmail
+
+Loads a User into this CurrentUser object.
+Takes the email address of the user to load.
+
+=cut
+
+sub LoadByEmail  {
+    my $self = shift;
+    my $identifier = shift;
+        
+    $self->LoadByCol("EmailAddress",$identifier);
+    
+}
+# }}}
+
+# {{{ sub LoadByGecos
+
+=head2 LoadByGecos
+
+Loads a User into this CurrentUser object.
+Takes a unix username as its only argument.
+
+=cut
+
+sub LoadByGecos  {
+    my $self = shift;
+    my $identifier = shift;
+        
+    $self->LoadByCol("Gecos",$identifier);
+    
+}
+# }}}
+
+# {{{ sub LoadByName
+
+=head2 LoadByName
+
+Loads a User into this CurrentUser object.
+Takes a Name.
+=cut
+
+sub LoadByName {
+    my $self = shift;
+    my $identifier = shift;
+    $self->LoadByCol("Name",$identifier);
+    
+}
+# }}}
+
+# {{{ sub Load 
+
+=head2 Load
+
+Loads a User into this CurrentUser object.
+Takes either an integer (users id column reference) or a Name
+The latter is deprecated. Instead, you should use LoadByName.
+Formerly, this routine also took email addresses. 
+
+=cut
+
+sub Load  {
+  my $self = shift;
+  my $identifier = shift;
+
+  #if it's an int, load by id. otherwise, load by name.
+  if ($identifier !~ /\D/) {
+    $self->SUPER::LoadById($identifier);
+  }
+  else {
+      # This is a bit dangerous, we might get false authen if somebody
+      # uses ambigous userids or real names:
+      $self->LoadByCol("Name",$identifier);
+  }
+}
+
+# }}}
+
+# {{{ sub IsPassword
+
+=head2 IsPassword
+
+Takes a password as a string.  Passes it off to IsPassword in this
+user's UserObj.  If it is the user's password and the user isn't
+disabled, returns 1.
+
+Otherwise, returns undef.
+
+=cut
+
+sub IsPassword { 
+  my $self = shift;
+  my $value = shift;
+  
+  return ($self->UserObj->IsPassword($value)); 
+}
+
+# }}}
+
+# {{{ sub Privileged
+
+=head2 Privileged
+
+Returns true if the current user can be granted rights and be
+a member of groups.
+
+=cut
+
+sub Privileged {
+    my $self = shift;
+    return ($self->UserObj->Privileged());
+}
+
+# }}}
+
+# {{{ Convenient ACL methods
+
+=head2 HasQueueRight
+
+calls $self->UserObj->HasQueueRight with the arguments passed in
+
+=cut
+
+sub HasQueueRight {
+       my $self = shift;
+       return ($self->UserObj->HasQueueRight(@_));
+}
+
+=head2 HasSystemRight
+
+calls $self->UserObj->HasSystemRight with the arguments passed in
+
+=cut
+
+
+sub HasSystemRight {
+       my $self = shift;
+       return ($self->UserObj->HasSystemRight(@_));
+}
+# }}}
+
+# {{{ sub HasRight
+
+=head2 HasSystemRight
+
+calls $self->UserObj->HasRight with the arguments passed in
+
+=cut
+
+sub HasRight {
+  my $self = shift;
+  return ($self->UserObj->HasRight(@_));
+}
+
+# }}}
+
+1;
diff --git a/rt/lib/RT/Date.pm b/rt/lib/RT/Date.pm
new file mode 100644 (file)
index 0000000..d569971
--- /dev/null
@@ -0,0 +1,436 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Date.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-2000 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+
+=head1 NAME
+
+  RT::Date - a simple Object Oriented date.
+
+=head1 SYNOPSIS
+
+  use RT::Date
+
+=head1 DESCRIPTION
+
+RT Date is a simple Date Object designed to be speedy and easy for RT to use
+
+The fact that it assumes that a time of 0 means "never" is probably a bug.
+
+=begin testing
+
+ok (require RT::Date);
+
+=end testing
+
+=head1 METHODS
+
+=cut
+
+
+package RT::Date;
+use Time::Local;
+use vars qw($MINUTE $HOUR $DAY $WEEK $MONTH $YEAR);
+
+$MINUTE = 60;
+$HOUR   = 60 * $MINUTE;
+$DAY    = 24 * $HOUR;
+$WEEK   = 7 * $DAY;
+$MONTH  = 4 * $WEEK;
+$YEAR   = 365 * $DAY;
+
+# {{{ sub new 
+
+sub new  {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self  = {};
+  bless ($self, $class);
+  $self->Unix(0);
+  return $self;
+}
+
+# }}}
+
+# {{{ sub Set
+
+=head2 sub Set
+
+takes a param hash with the fields 'Format' and 'Value'
+
+if $args->{'Format'} is 'unix', takes the number of seconds since the epoch 
+
+If $args->{'Format'} is ISO, tries to parse an ISO date.
+
+If $args->{'Format'} is 'unknown', require Date::Parse and make it figure things
+out. This is a heavyweight operation that should never be called from within 
+RT's core. But it's really useful for something like the textbox date entry
+where we let the user do whatever they want.
+
+If $args->{'Value'}  is 0, assumes you mean never.
+
+
+=cut
+
+sub Set {
+    my $self = shift;
+    my %args = ( Format => 'unix',
+                Value => time,
+                @_);
+    if (($args{'Value'} =~ /^\d*$/) and ($args{'Value'} == 0)) {
+       $self->Unix(-1);
+       return($self->Unix());
+    }
+
+    if ($args{'Format'} =~ /^unix$/i) {
+       $self->Unix($args{'Value'});
+    }
+    
+    elsif ($args{'Format'} =~ /^(sql|datemanip|iso)$/i) {
+       
+       if (($args{'Value'} =~ /^(\d{4}?)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/) ||
+           ($args{'Value'} =~ /^(\d{4}?)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/) ||
+           ($args{'Value'} =~ /^(\d{4}?)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\+00$/) ||
+           ($args{'Value'} =~ /^(\d{4}?)(\d\d)(\d\d)(\d\d):(\d\d):(\d\d)$/)) {
+           
+        my $year = $1;
+           my $mon = $2;
+           my $mday = $3;
+           my $hours = $4;
+           my $min = $5;
+           my $sec = $6;
+           
+           #timegm expects month as 0->11
+           $mon--;
+           
+           #now that we've parsed it, deal with the case where everything
+           #was 0
+            if ($mon == -1) {
+                   $self->Unix(-1);
+               } else {
+
+                   #Dateamnip strings aren't in GMT.
+                   if ($args{'Format'} =~ /^datemanip$/i) {
+                       $self->Unix(timelocal($sec,$min,$hours,$mday,$mon,$year));
+                   }
+                   #ISO and SQL dates are in GMT
+                   else {
+                       $self->Unix(timegm($sec,$min,$hours,$mday,$mon,$year));
+                   }
+                   
+                   $self->Unix(-1) unless $self->Unix;
+               }
+   }  
+       else {
+           use Carp;
+           Carp::cluck;
+           $RT::Logger->debug( "Couldn't parse date $args{'Value'} as a $args{'Format'}");
+           
+       }
+    }
+    elsif ($args{'Format'} =~ /^unknown$/i) {
+        require Date::Parse;
+        #Convert it to an ISO format string 
+        
+       my $date = Date::Parse::str2time($args{'Value'});
+        
+       #This date has now been set to a date in the _local_ timezone.
+       #since ISO dates are known to be in GMT (for RT's purposes);
+       
+       $RT::Logger->debug("RT::Date used date::parse to make ".$args{'Value'} . " $date\n");
+        
+       
+       return ($self->Set( Format => 'unix', Value => "$date"));
+    }                                                    
+    else {
+       die "Unknown Date format: ".$args{'Format'}."\n";
+    }
+    
+    return($self->Unix());
+}
+
+# }}}
+
+# {{{ sub SetToMidnight 
+
+=head2 SetToMidnight
+
+Sets the date to midnight (at the beginning of the day) GMT
+Returns the unixtime at midnight.
+
+=cut
+
+sub SetToMidnight {
+    my $self = shift;
+    
+    use Time::Local;
+    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = gmtime($self->Unix);
+    $self->Unix(timegm (0,0,0,$mday,$mon,$year,$wday,$yday));
+    
+    return ($self->Unix);
+    
+    
+}
+
+
+# }}}
+
+# {{{ sub SetToNow
+sub SetToNow {
+       my $self = shift;
+       return($self->Set(Format => 'unix', Value => time))
+}
+# }}}
+
+# {{{ sub Diff
+
+=head2 Diff
+
+Takes either an RT::Date object or the date in unixtime format as a string
+
+Returns the differnce between $self and that time as a number of seconds
+
+=cut
+
+sub Diff {
+    my $self = shift;
+    my $other = shift;
+
+    if (ref($other) eq 'RT::Date') {
+       $other=$other->Unix;
+    }
+    return ($self->Unix - $other);
+}
+# }}}
+
+# {{{ sub DiffAsString
+
+=head2 sub DiffAsString
+
+Takes either an RT::Date object or the date in unixtime format as a string
+
+Returns the differnce between $self and that time as a number of seconds as
+as string fit for human consumption
+
+=cut
+
+sub DiffAsString {
+    my $self = shift;
+    my $other = shift;
+
+
+    if ($other < 1) {
+       return ("");
+    }
+    if ($self->Unix < 1) {
+       return("");
+    }
+    my $diff = $self->Diff($other);
+
+    return ($self->DurationAsString($diff));
+}
+# }}}
+
+# {{{ sub DurationAsString
+
+=head2 DurationAsString
+
+Takes a number of seconds. returns a string describing that duration
+
+=cut
+
+sub DurationAsString{
+
+    my $self=shift;
+    my $duration = shift;
+    
+    my ($negative, $s);
+    
+    $negative = 'ago' if ($duration < 0);
+
+    $duration = abs($duration);
+
+    if($duration < $MINUTE) {
+       $s=$duration;
+       $string="sec";
+    } elsif($duration < (2 * $HOUR)) {
+       $s = int($duration/$MINUTE);
+       $string="min";
+    } elsif($duration < (2 * $DAY)) {
+       $s = int($duration/$HOUR);
+       $string="hours";
+    } elsif($duration < (2 * $WEEK)) {
+       $s = int($duration/$DAY);
+       $string="days";
+    } elsif($duration < (2 * $MONTH)) {
+       $s = int($duration/$WEEK);
+       $string="weeks";
+    } elsif($duration < $YEAR) {
+       $s = int($duration/$MONTH);
+       $string="months";
+    } else {
+       $s = int($duration/$YEAR);
+       $string="years";
+    }
+    
+    return ("$s $string $negative");
+}
+
+# }}}
+
+# {{{ sub AgeAsString
+
+=head2 sub AgeAsString
+
+Takes nothing
+
+Returns a string that's the differnce between the time in the object and now
+
+=cut
+
+sub AgeAsString {
+    my $self = shift;
+    return ($self->DiffAsString(time));
+    }
+# }}}
+
+# {{{ sub AsString
+
+=head2 sub AsString
+
+Returns the object\'s time as a string with the current timezone.
+
+=cut
+
+sub AsString {
+    my $self = shift;
+    return ("Not set") if ($self->Unix <= 0);
+
+    return (scalar(localtime($self->Unix)));
+}
+# }}}
+
+# {{{ sub AddSeconds
+
+=head2 sub AddSeconds
+
+Takes a number of seconds as a string
+
+Returns the new time
+
+=cut
+
+sub AddSeconds {
+    my $self = shift;
+    my $delta = shift;
+    
+    $self->Set(Format => 'unix', Value => ($self->Unix + $delta));
+    
+    return ($self->Unix);
+    
+
+}
+
+# }}}
+
+# {{{ sub AddDays
+
+=head2 AddDays $DAYS
+
+Adds 24 hours * $DAYS to the current time
+
+=cut
+
+sub AddDays {
+    my $self = shift;
+    my $days = shift;
+    $self->AddSeconds($days * $DAY);
+    
+}
+
+# }}}
+
+# {{{ sub AddDay
+
+=head2 AddDay
+
+Adds 24 hours to the current time
+
+=cut
+
+sub AddDay {
+    my $self = shift;
+    $self->AddSeconds($DAY);
+    
+}
+
+# }}}
+
+# {{{ sub Unix
+
+=head2 sub Unix [unixtime]
+
+Optionally takes a date in unix seconds since the epoch format.
+Returns the number of seconds since the epoch
+
+=cut
+
+sub Unix {
+    my $self = shift;
+    
+    $self->{'time'} = shift if (@_);
+    
+    return ($self->{'time'});
+}
+# }}}
+
+# {{{ sub ISO
+
+=head2 ISO
+
+Takes nothing
+
+Returns the object's date in ISO format
+
+=cut
+
+sub ISO {
+    my $self=shift;
+    my    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst, $date) ;
+    
+    return ('1970-01-01 00:00:00') if ($self->Unix == -1);
+
+    #  0    1    2     3     4    5     6     7     8
+    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($self->Unix);
+    #make the year YYYY
+    $year+=1900;
+
+    #the month needs incrementing, as gmtime returns 0-11
+    $mon++;
+        
+    $date = sprintf("%04d-%02d-%02d %02d:%02d:%02d", $year,$mon,$mday, $hour,$min,$sec);
+    
+    return ($date);
+}
+
+# }}}
+
+
+# {{{ sub LocalTimezone 
+=head2 LocalTimezone
+
+  Returns the current timezone. For now, draws off a system timezone, RT::Timezone. Eventually, this may
+pull from a 'Timezone' attribute of the CurrentUser
+
+=cut
+
+sub LocalTimezone {
+    my $self = shift;
+    
+    return ($RT::Timezone);
+}
+
+# }}}
+
+
+
+1;
diff --git a/rt/lib/RT/EasySearch.pm b/rt/lib/RT/EasySearch.pm
new file mode 100755 (executable)
index 0000000..bcbfa01
--- /dev/null
@@ -0,0 +1,115 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attic/EasySearch.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::EasySearch - a baseclass for RT collection objects
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::EasySearch);
+
+=end testing
+
+
+=cut
+
+package RT::EasySearch;
+use DBIx::SearchBuilder;
+@ISA= qw(DBIx::SearchBuilder);
+
+# {{{ sub _Init 
+sub _Init  {
+    my $self = shift;
+    
+    $self->{'user'} = shift;
+    unless(defined($self->CurrentUser)) {
+       use Carp;
+       Carp::confess("$self was created without a CurrentUser");
+       $RT::Logger->err("$self was created without a CurrentUser\n"); 
+       return(0);
+    }
+    $self->SUPER::_Init( 'Handle' => $RT::Handle);
+}
+# }}}
+
+# {{{ sub LimitToEnabled
+
+=head2 LimitToEnabled
+
+Only find items that haven\'t been disabled
+
+=cut
+
+sub LimitToEnabled {
+    my $self = shift;
+    
+    $self->Limit( FIELD => 'Disabled',
+                 VALUE => '0',
+                 OPERATOR => '=' );
+}
+# }}}
+
+# {{{ sub LimitToDisabled
+
+=head2 LimitToDeleted
+
+Only find items that have been deleted.
+
+=cut
+
+sub LimitToDeleted {
+    my $self = shift;
+    
+    $self->{'find_disabled_rows'} = 1;
+    $self->Limit( FIELD => 'Disabled',
+                 OPERATOR => '=',
+                 VALUE => '1'
+               );
+}
+# }}}
+
+
+# {{{ sub Limit 
+
+=head2 Limit PARAMHASH
+
+This Limit sub calls SUPER::Limit, but defaults "CASESENSITIVE" to 1, thus
+making sure that by default lots of things don't do extra work trying to 
+match lower(colname) agaist lc($val);
+
+=cut
+
+sub Limit {
+       my $self = shift;
+       my %args = ( CASESENSITIVE => 1,
+                    @_ );
+
+   return $self->SUPER::Limit(%args);
+}
+
+# {{{ sub CurrentUser 
+
+=head2 CurrentUser
+
+  Returns the current user as an RT::User object.
+
+=cut
+
+sub CurrentUser  {
+  my $self = shift;
+  return ($self->{'user'});
+}
+# }}}
+    
+
+1;
+
+
diff --git a/rt/lib/RT/Group.pm b/rt/lib/RT/Group.pm
new file mode 100755 (executable)
index 0000000..005601f
--- /dev/null
@@ -0,0 +1,364 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Group.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Copyright 2000 Jesse Vincent <jesse@fsck.com>
+# Released under the terms of the GNU Public License
+#
+#
+
+=head1 NAME
+
+  RT::Group - RT\'s group object
+
+=head1 SYNOPSIS
+
+  use RT::Group;
+my $group = new RT::Group($CurrentUser);
+
+=head1 DESCRIPTION
+
+An RT group object.
+
+=head1 AUTHOR
+
+Jesse Vincent, jesse@fsck.com
+
+=head1 SEE ALSO
+
+RT
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Group);
+
+=end testing
+
+=cut
+
+
+package RT::Group;
+use RT::Record;
+use RT::GroupMember;
+use RT::ACE;
+
+use vars qw|@ISA|;
+@ISA= qw(RT::Record);
+
+
+# {{{ sub _Init
+sub _Init  {
+  my $self = shift; 
+  $self->{'table'} = "Groups";
+  return ($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _Accessible 
+sub _Accessible  {
+    my $self = shift;
+    my %Cols = (
+               Name => 'read/write',
+               Description => 'read/write',
+               Pseudo => 'read'
+              );
+    return $self->SUPER::_Accessible(@_, %Cols);
+}
+# }}}
+
+# {{{ sub Load 
+
+=head2 Load
+
+Load a group object from the database. Takes a single argument.
+If the argument is numerical, load by the column 'id'. Otherwise, load by
+the "Name" column which is the group's textual name
+
+=cut
+
+sub Load  {
+    my $self = shift;
+    my $identifier = shift || return undef;
+    
+    #if it's an int, load by id. otherwise, load by name.
+    if ($identifier !~ /\D/) {
+       $self->SUPER::LoadById($identifier);
+    }
+    else {
+       $self->LoadByCol("Name",$identifier);
+    }
+}
+
+# }}}
+
+# {{{ sub Create
+
+=head2 Create
+
+Takes a paramhash with three named arguments: Name, Description and Pseudo.
+Pseudo is used internally by RT for certain special ACL decisions.
+
+=cut
+
+sub Create {
+    my $self = shift;
+    my %args = ( Name => undef,
+                Description => undef,
+                Pseudo => 0,
+                @_);
+    
+    unless ($self->CurrentUser->HasSystemRight('AdminGroups')) {
+       $RT::Logger->warning($self->CurrentUser->Name ." Tried to create a group without permission.");
+       return(0, 'Permission Denied');
+    }
+    
+    my $retval = $self->SUPER::Create(Name => $args{'Name'},
+                                     Description => $args{'Description'},
+                                     Pseudo => $args{'Pseudo'});
+    
+    return ($retval);
+}
+
+# }}}
+
+# {{{ sub Delete
+
+=head2 Delete
+
+Delete this object
+
+=cut
+
+sub Delete {
+    my $self = shift;
+    
+    unless ($self->CurrentUser->HasSystemRight('AdminGroups')) {
+       return (0, 'Permission Denied');
+    }
+    
+    return($self->SUPER::Delete(@_));    
+}
+
+# }}}
+
+# {{{ MembersObj
+
+=head2 MembersObj
+
+Returns an RT::GroupMembers object of this group's members.
+
+=cut
+
+sub MembersObj {
+    my $self = shift;
+    unless (defined $self->{'members_obj'}) {
+       use RT::GroupMembers;
+        $self->{'members_obj'} = new RT::GroupMembers($self->CurrentUser);
+       
+       #If we don't have rights, don't include any results
+       $self->{'members_obj'}->LimitToGroup($self->id);
+       
+    }
+    return ($self->{'members_obj'});
+    
+}
+
+# }}}
+
+# {{{ AddMember
+
+=head2 AddMember
+
+AddMember adds a user to this group.  It takes a user id.
+Returns a two value array. the first value is true on successful 
+addition or 0 on failure.  The second value is a textual status msg.
+
+=cut
+
+sub AddMember {
+    my $self = shift;
+    my $new_member = shift;
+
+    my $new_member_obj = new RT::User($self->CurrentUser);
+    $new_member_obj->Load($new_member);
+    
+    unless ($self->CurrentUser->HasSystemRight('AdminGroups')) {
+        #User has no permission to be doing this
+        return(0, "Permission Denied");
+    }
+
+    unless ($new_member_obj->Id) {
+       $RT::Logger->debug("Couldn't find user $new_member");
+       return(0, "Couldn't find user");
+    }  
+
+    if ($self->HasMember($new_member_obj->Id)) {
+        #User is already a member of this group. no need to add it
+        return(0, "Group already has member");
+    }
+    
+    my $member_object = new RT::GroupMember($self->CurrentUser);
+    $member_object->Create( UserId => $new_member_obj->Id, 
+                           GroupId => $self->id );
+    return(1, "Member added");
+}
+
+# }}}
+
+# {{{ HasMember
+
+=head2 HasMember
+
+Takes a user Id and returns a GroupMember Id if that user is a member of 
+this group.
+Returns undef if the user isn't a member of the group or if the current
+user doesn't have permission to find out. Arguably, it should differentiate
+between ACL failure and non membership.
+
+=cut
+
+sub HasMember {
+    my $self = shift;
+    my $user_id = shift;
+
+    #Try to cons up a member object using "LoadByCols"
+
+    my $member_obj = new RT::GroupMember($self->CurrentUser);
+    $member_obj->LoadByCols( UserId => $user_id, GroupId => $self->id);
+
+    #If we have a member object
+    if (defined $member_obj->id) {
+        return ($member_obj->id);
+    }
+
+    #If Load returns no objects, we have an undef id. 
+    else {
+        return(undef);
+    } 
+}
+
+# }}}
+
+# {{{ DeleteMember
+
+=head2 DeleteMember
+
+Takes the user id of a member.
+If the current user has apropriate rights,
+removes that GroupMember from this group.
+Returns a two value array. the first value is true on successful 
+addition or 0 on failure.  The second value is a textual status msg.
+
+=cut
+
+sub DeleteMember {
+    my $self = shift;
+    my $member = shift;
+
+    unless ($self->CurrentUser->HasSystemRight('AdminGroups')) {
+        #User has no permission to be doing this
+        return(0,"Permission Denied");
+    }
+
+    my $member_user_obj = new RT::User($self->CurrentUser);
+    $member_user_obj->Load($member);
+    
+    unless ($member_user_obj->Id) {
+       $RT::Logger->debug("Couldn't find user $member");
+       return(0, "User not found");
+    }  
+
+    my $member_obj = new RT::GroupMember($self->CurrentUser);
+    unless ($member_obj->LoadByCols ( UserId => $member_user_obj->Id,
+                                     GroupId => $self->Id )) {
+       return(0, "Couldn\'t load member");  #couldn\'t load member object
+    }
+    
+    #If we couldn't load it, return undef.
+    unless ($member_obj->Id()) {
+       return (0, "Group has no such member");
+    }  
+    
+    #Now that we've checked ACLs and sanity, delete the groupmember
+    my $val = $member_obj->Delete();
+    if ($val) {
+       return ($val, "Member deleted");
+    }
+    else {
+       return (0, "Member not deleted");
+    }
+}
+
+# }}}
+
+# {{{ ACL Related routines
+
+# {{{ GrantQueueRight
+
+=head2 GrantQueueRight
+
+Grant a queue right to this group.  Takes a paramhash of which the elements
+RightAppliesTo and RightName are important.
+
+=cut
+
+sub GrantQueueRight {
+    
+    my $self = shift;
+    my %args = ( RightScope => 'Queue',
+                RightName => undef,
+                RightAppliesTo => undef,
+                PrincipalType => 'Group',
+                PrincipalId => $self->Id,
+                @_);
+   
+    #ACLs get checked in ACE.pm
+    
+    my $ace = new RT::ACE($self->CurrentUser);
+    
+    return ($ace->Create(%args));
+}
+
+# }}}
+
+# {{{ GrantSystemRight
+
+=head2 GrantSystemRight
+
+Grant a system right to this group. 
+The only element that's important to set is RightName.
+
+=cut
+sub GrantSystemRight {
+    
+    my $self = shift;
+    my %args = ( RightScope => 'System',
+                RightName => undef,
+                RightAppliesTo => 0,
+                PrincipalType => 'Group',
+                PrincipalId => $self->Id,
+                @_);
+    
+    # ACLS get checked in ACE.pm
+    
+    my $ace = new RT::ACE($self->CurrentUser);
+    return ($ace->Create(%args));
+}
+
+
+# }}}
+
+
+# {{{ sub _Set
+sub _Set {
+    my $self = shift;
+
+    unless ($self->CurrentUser->HasSystemRight('AdminGroups')) {
+       return (0, 'Permission Denied');
+    }  
+
+    return ($self->SUPER::_Set(@_));
+
+}
+# }}}
diff --git a/rt/lib/RT/GroupMember.pm b/rt/lib/RT/GroupMember.pm
new file mode 100755 (executable)
index 0000000..69de50b
--- /dev/null
@@ -0,0 +1,136 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/GroupMember.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Copyright 2000 Jesse Vincent <jesse@fsck.com>
+# Released under the terms of the GNU Public License
+
+=head1 NAME
+
+  RT::GroupMember - a member of an RT Group
+
+=head1 SYNOPSIS
+
+RT::GroupMember should never be called directly. It should generally
+only be accessed through the helper functions in RT::Group;
+
+=head1 DESCRIPTION
+
+
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::GroupMember);
+
+=end testing
+
+
+=cut
+
+package RT::GroupMember;
+use RT::Record;
+use vars qw|@ISA|;
+@ISA= qw(RT::Record);
+
+# {{{ sub _Init
+sub _Init {
+  my $self = shift; 
+  $self->{'table'} = "GroupMembers";
+  return($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _Accessible 
+sub _Accessible  {
+    my $self = shift;
+    my %Cols = (
+               GroupId => 'read',
+               UserId => 'read'
+               );
+
+    return $self->SUPER::_Accessible(@_, %Cols);
+}
+# }}}
+
+# {{{ sub Create
+
+# a helper method for Add
+
+sub Create {
+    my $self = shift;
+    my %args = ( GroupId => undef,
+                UserId => undef,
+                @_
+              );
+    
+    unless( $self->CurrentUser->HasSystemRight('AdminGroups')) {
+       return (0, 'Permission Denied');
+    }
+
+    return ($self->SUPER::Create(GroupId => $args{'GroupId'},
+                                UserId => $args{'UserId'}))
+}
+# }}}
+
+# {{{ sub Add
+
+=head2 Add
+
+Takes a paramhash of UserId and GroupId.  makes that user a memeber
+of that group
+
+=cut
+
+sub Add {
+    my $self = shift;
+    return ($self->Create(@_));
+}
+# }}}
+
+# {{{ sub Delete
+
+=head2 Delete
+
+Takes no arguments. deletes the currently loaded member from the 
+group in question.
+
+=cut
+
+sub Delete {
+    my $self = shift;
+    unless ($self->CurrentUser->HasSystemRight('AdminGroups')) {
+       return (0, 'Permission Denied');
+    }
+    return($self->SUPER::Delete(@_));
+}
+
+# }}}
+
+# {{{ sub UserObj
+
+=head2 UserObj
+
+Returns an RT::User object for the user specified by $self->UserId
+
+=cut
+
+sub UserObj {
+    my $self = shift;
+    unless (defined ($self->{'user_obj'})) {
+        $self->{'user_obj'} = new RT::User($self->CurrentUser);
+        $self->{'user_obj'}->Load($self->UserId);
+    }
+    return($self->{'user_obj'});
+}
+
+# {{{ sub _Set
+sub _Set {
+    my $self = shift;
+    unless ($self->CurrentUser->HasSystemRight('AdminGroups')) {
+       return (0, 'Permission Denied');
+    }
+    return($self->SUPER::_Set(@_));
+}
+# }}}
diff --git a/rt/lib/RT/GroupMembers.pm b/rt/lib/RT/GroupMembers.pm
new file mode 100755 (executable)
index 0000000..a90a2a8
--- /dev/null
@@ -0,0 +1,73 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/GroupMembers.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::GroupMembers - a collection of RT::GroupMember objects
+
+=head1 SYNOPSIS
+
+  use RT::GroupMembers;
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::GroupMembers);
+
+=end testing
+
+=cut
+
+package RT::GroupMembers;
+use RT::EasySearch;
+use RT::GroupMember;
+
+@ISA= qw(RT::EasySearch);
+
+
+# {{{ sub _Init  
+sub _Init   {
+  my $self = shift;
+  $self->{'table'} = "GroupMembers";
+  $self->{'primary_key'} = "id";
+  return ( $self->SUPER::_Init(@_) );
+}
+# }}}
+
+# {{{ sub LimitToGroup
+
+=head2 LimitToGroup
+
+Takes a group id as its only argument.  Limits the current search to that
+group object
+
+=cut
+
+sub LimitToGroup {
+    my $self = shift;
+    my $group = shift;
+
+    return ($self->Limit( 
+                         VALUE => "$group",
+                         FIELD => 'GroupId',
+                         ENTRYAGGREGATOR => 'OR',
+                         ));
+
+}
+# }}}
+
+# {{{ sub NewItem 
+
+sub NewItem  {
+    my $self = shift;
+    return(RT::GroupMember->new($self->CurrentUser))
+}
+
+# }}}
+1;
diff --git a/rt/lib/RT/Groups.pm b/rt/lib/RT/Groups.pm
new file mode 100755 (executable)
index 0000000..f44f1fd
--- /dev/null
@@ -0,0 +1,100 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Groups.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Groups - a collection of RT::Group objects
+
+=head1 SYNOPSIS
+
+  use RT::Groups;
+  my $groups = $RT::Groups->new($CurrentUser);
+  $groups->LimitToReal();
+  while (my $group = $groups->Next()) {
+     print $group->Id ." is a group id\n";
+  }
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Groups);
+
+=end testing
+
+=cut
+
+package RT::Groups;
+use RT::EasySearch;
+use RT::Groups;
+
+@ISA= qw(RT::EasySearch);
+
+# {{{ sub _Init
+
+sub _Init { 
+  my $self = shift;
+  $self->{'table'} = "Groups";
+  $self->{'primary_key'} = "id";
+
+  $self->OrderBy( ALIAS => 'main',
+                 FIELD => 'Name',
+                 ORDER => 'ASC');
+
+
+  return ( $self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ LimitToReal
+
+=head2 LimitToReal
+
+Make this groups object return only "real" groups, which can be
+granted rights and have members assigned to them
+
+=cut
+
+sub LimitToReal {
+    my $self = shift;
+
+    return ($self->Limit( FIELD => 'Pseudo',
+                         VALUE => '0',
+                         OPERATOR => '='));
+
+}
+# }}}
+
+# {{{ sub LimitToPseudo
+
+=head2 LimitToPseudo
+
+Make this groups object return only "pseudo" groups, which can be
+granted rights but whose membership lists are determined dynamically.
+
+=cut
+  
+  sub LimitToPseudo {
+    my $self = shift;
+
+    return ($self->Limit( FIELD => 'Pseudo',
+                         VALUE => '1',
+                         OPERATOR => '='));
+
+}
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem  {
+  my $self = shift;
+  return (RT::Group->new($self->CurrentUser));
+}
+# }}}
+
+
+1;
+
diff --git a/rt/lib/RT/Handle.pm b/rt/lib/RT/Handle.pm
new file mode 100644 (file)
index 0000000..6b74f36
--- /dev/null
@@ -0,0 +1,53 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Handle.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Handle - RT's database handle
+
+=head1 SYNOPSIS
+
+  use RT::Handle;
+
+=head1 DESCRIPTION
+
+=begin testing
+
+ok(require RT::Handle);
+
+=end testing
+
+=head1 METHODS
+
+=cut
+
+package RT::Handle;
+
+eval "use DBIx::SearchBuilder::Handle::$RT::DatabaseType;
+
+\@ISA= qw(DBIx::SearchBuilder::Handle::$RT::DatabaseType);";
+
+#TODO check for errors here.
+
+=head2 Connect
+
+Takes nothing. Calls SUPER::Connect with the needed args
+
+=cut
+
+sub Connect {
+my $self=shift;
+
+# Unless the database port is a positive integer, we really don't want to pass it.
+$RT::DatabasePort = undef unless (defined $RT::DatabasePort && $RT::DatabasePort =~ /^(\d+)$/);
+
+$self->SUPER::Connect(Host => $RT::DatabaseHost, 
+                        Database => $RT::DatabaseName, 
+                        User => $RT::DatabaseUser,
+                        Password => $RT::DatabasePassword,
+                        Port => $RT::DatabasePort,
+                        Driver => $RT::DatabaseType,
+                        RequireSSL => $RT::DatabaseRequireSSL,
+                       );
+   
+}
+1;
diff --git a/rt/lib/RT/Interface/CLI.pm b/rt/lib/RT/Interface/CLI.pm
new file mode 100644 (file)
index 0000000..a3bf92d
--- /dev/null
@@ -0,0 +1,224 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Interface/CLI.pm,v 1.1 2002-08-12 06:17:08 ivan Exp $
+# RT is (c) 1996-2001 Jesse Vincent <jesse@fsck.com>
+
+package RT::Interface::CLI;
+
+use strict;
+
+
+BEGIN {
+    use Exporter ();
+    use vars qw ($VERSION  @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+    
+    # set the version for version checking
+    $VERSION = do { my @r = (q$Revision: 1.1 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # must be all one line, for MakeMaker
+    
+    @ISA         = qw(Exporter);
+    
+    # your exported package globals go here,
+    # as well as any optionally exported functions
+    @EXPORT_OK   = qw(&CleanEnv &LoadConfig &DBConnect 
+                     &GetCurrentUser &GetMessageContent &debug);
+}
+
+=head1 NAME
+
+  RT::Interface::CLI - helper functions for creating a commandline RT interface
+
+=head1 SYNOPSIS
+
+  use lib "!!RT_LIB_PATH!!";
+  use lib "!!RT_ETC_PATH!!";
+
+  use RT::Interface::CLI  qw(CleanEnv LoadConfig DBConnect 
+                          GetCurrentUser GetMessageContent);
+
+  #Clean out all the nasties from the environment
+  CleanEnv();
+
+  #Load etc/config.pm and drop privs
+  LoadConfig();
+
+  #Connect to the database and get RT::SystemUser and RT::Nobody loaded
+  DBConnect();
+
+
+  #Get the current user all loaded
+  my $CurrentUser = GetCurrentUser();
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::Interface::CLI);
+
+=end testing
+
+=cut
+
+
+=head2 CleanEnv
+
+Removes some of the nastiest nasties from the user\'s environment.
+
+=cut
+
+sub CleanEnv {
+    $ENV{'PATH'} = '/bin:/usr/bin';    # or whatever you need
+    $ENV{'CDPATH'} = '' if defined $ENV{'CDPATH'};
+    $ENV{'SHELL'} = '/bin/sh' if defined $ENV{'SHELL'};
+    $ENV{'ENV'} = '' if defined $ENV{'ENV'};
+    $ENV{'IFS'} = ''           if defined $ENV{'IFS'};
+}
+
+
+
+=head2 LoadConfig
+
+Loads RT's config file and then drops setgid privileges.
+
+=cut
+
+sub LoadConfig {
+    
+    #This drags in  RT's config.pm
+    use config;
+    
+}      
+
+
+
+=head2 DBConnect
+
+  Calls RT::Init, which creates a database connection and then creates $RT::Nobody
+  and $RT::SystemUser
+
+=cut
+
+
+sub DBConnect {
+    use RT;
+    RT::Init();
+}
+
+
+
+# {{{ sub GetCurrentUser 
+
+=head2 GetCurrentUser
+
+  Figures out the uid of the current user and returns an RT::CurrentUser object
+loaded with that user.  if the current user isn't found, returns a copy of RT::Nobody.
+
+=cut
+sub GetCurrentUser  {
+    
+    my ($Gecos, $CurrentUser);
+    
+    require RT::CurrentUser;
+    
+    #Instantiate a user object
+    
+    $Gecos=(getpwuid($<))[0];
+
+    #If the current user is 0, then RT will assume that the User object
+    #is that of the currentuser.
+
+    $CurrentUser = new RT::CurrentUser();
+    $CurrentUser->LoadByGecos($Gecos);
+    
+    unless ($CurrentUser->Id) {
+       $RT::Logger->debug("No user with a unix login of '$Gecos' was found. ");
+    }
+    return($CurrentUser);
+}
+# }}}
+
+# {{{ sub GetMessageContent
+
+=head2 GetMessageContent
+
+Takes two arguments a source file and a boolean "edit".  If the source file
+is undef or "", assumes an empty file.  Returns an edited file as an 
+array of lines.
+
+=cut
+
+sub GetMessageContent {
+    my %args = (  Source => undef,
+                 Content => undef,
+                 Edit => undef,
+                 CurrentUser => undef,
+                @_);
+    my $source = $args{'Source'};
+
+    my $edit = $args{'Edit'};
+    
+    my $currentuser = $args{'CurrentUser'};
+    my @lines;
+
+    use File::Temp qw/ tempfile/;
+    
+    #Load the sourcefile, if it's been handed to us
+    if ($source) {
+       open (SOURCE, "<$source");
+       @lines = (<SOURCE>);
+       close (SOURCE);
+    }
+    elsif ($args{'Content'}) {
+       @lines = split('\n',$args{'Content'});
+    }
+    #get us a tempfile.
+    my ($fh, $filename) = tempfile();
+       
+    #write to a tmpfile
+    for (@lines) {
+       print $fh $_;
+    }
+    close ($fh);
+    
+    #Edit the file if we need to
+    if ($edit) {       
+
+       unless ($ENV{'EDITOR'}) {
+           $RT::Logger->crit('No $EDITOR variable defined'. "\n");
+           return undef;
+       }
+       system ($ENV{'EDITOR'}, $filename);
+    }  
+    
+    open (READ, "<$filename");
+    my @newlines = (<READ>);
+    close (READ);
+
+    unlink ($filename) unless (debug());
+    return(\@newlines);
+    
+}
+
+# }}}
+
+# {{{ sub debug
+
+sub debug {
+    my $val = shift;
+    my ($debug);
+    if ($val) {
+       $RT::Logger->debug($val."\n");
+       if ($debug) {
+           print STDERR "$val\n";
+       }
+    }
+    if ($debug) {
+       return(1);
+    }  
+}
+
+# }}}
+
+
+1;
diff --git a/rt/lib/RT/Interface/Email.pm b/rt/lib/RT/Interface/Email.pm
new file mode 100755 (executable)
index 0000000..e954360
--- /dev/null
@@ -0,0 +1,581 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Interface/Email.pm,v 1.1 2002-08-12 06:17:08 ivan Exp $
+# RT is (c) 1996-2001 Jesse Vincent <jesse@fsck.com>
+
+package RT::Interface::Email;
+
+use strict;
+use Mail::Address;
+use MIME::Entity;
+
+BEGIN {
+    use Exporter ();
+    use vars qw ($VERSION  @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+    
+    # set the version for version checking
+    $VERSION = do { my @r = (q$Revision: 1.1 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # must be all one line, for MakeMaker
+    
+    @ISA         = qw(Exporter);
+    
+    # your exported package globals go here,
+    # as well as any optionally exported functions
+    @EXPORT_OK   = qw(&CleanEnv 
+                     &LoadConfig 
+                     &DBConnect 
+                     &GetCurrentUser
+                     &GetMessageContent
+                     &CheckForLoops 
+                     &CheckForSuspiciousSender
+                     &CheckForAutoGenerated 
+                     &ParseMIMEEntityFromSTDIN
+                     &ParseTicketId 
+                     &MailError 
+                     &ParseCcAddressesFromHead
+                     &ParseSenderAddressFromHead 
+                     &ParseErrorsToAddressFromHead
+              &ParseAddressFromHeader
+
+
+                     &debug);
+}
+
+=head1 NAME
+
+  RT::Interface::CLI - helper functions for creating a commandline RT interface
+
+=head1 SYNOPSIS
+
+  use lib "!!RT_LIB_PATH!!";
+  use lib "!!RT_ETC_PATH!!";
+
+  use RT::Interface::Email  qw(CleanEnv LoadConfig DBConnect 
+                             );
+
+  #Clean out all the nasties from the environment
+  CleanEnv();
+
+  #Load etc/config.pm and drop privs
+  LoadConfig();
+
+  #Connect to the database and get RT::SystemUser and RT::Nobody loaded
+  DBConnect();
+
+
+  #Get the current user all loaded
+  my $CurrentUser = GetCurrentUser();
+
+=head1 DESCRIPTION
+
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::Interface::Email);
+
+=end testing
+
+
+=head1 METHODS
+
+=cut
+
+
+=head2 CleanEnv
+
+Removes some of the nastiest nasties from the user\'s environment.
+
+=cut
+
+sub CleanEnv {
+    $ENV{'PATH'} = '/bin:/usr/bin';    # or whatever you need
+    $ENV{'CDPATH'} = '' if defined $ENV{'CDPATH'};
+    $ENV{'SHELL'} = '/bin/sh' if defined $ENV{'SHELL'};
+    $ENV{'ENV'} = '' if defined $ENV{'ENV'};
+    $ENV{'IFS'} = '' if defined $ENV{'IFS'};
+}
+
+
+
+=head2 LoadConfig
+
+Loads RT's config file and then drops setgid privileges.
+
+=cut
+
+sub LoadConfig {
+    
+    #This drags in  RT's config.pm
+    use config;
+    
+}      
+
+
+
+=head2 DBConnect
+
+  Calls RT::Init, which creates a database connection and then creates $RT::Nobody
+  and $RT::SystemUser
+
+=cut
+
+
+sub DBConnect {
+    use RT;
+    RT::Init();
+}
+
+
+
+# {{{ sub debug
+
+sub debug {
+    my $val = shift;
+    my ($debug);
+    if ($val) {
+       $RT::Logger->debug($val."\n");
+       if ($debug) {
+           print STDERR "$val\n";
+       }
+    }
+    if ($debug) {
+       return(1);
+    }  
+}
+
+# }}}
+
+
+# {{{ sub CheckForLoops 
+
+sub CheckForLoops  {
+    my $head = shift;
+    
+    #If this instance of RT sent it our, we don't want to take it in
+    my $RTLoop = $head->get("X-RT-Loop-Prevention") || "";
+    chomp ($RTLoop); #remove that newline
+    if ($RTLoop eq "$RT::rtname") {
+       return (1);
+    }
+    
+    # TODO: We might not trap the case where RT instance A sends a mail
+    # to RT instance B which sends a mail to ...
+    return (undef);
+}
+
+# }}}
+
+# {{{ sub CheckForSuspiciousSender
+
+sub CheckForSuspiciousSender {
+    my $head = shift;
+
+    #if it's from a postmaster or mailer daemon, it's likely a bounce.
+    
+    #TODO: better algorithms needed here - there is no standards for
+    #bounces, so it's very difficult to separate them from anything
+    #else.  At the other hand, the Return-To address is only ment to be
+    #used as an error channel, we might want to put up a separate
+    #Return-To address which is treated differently.
+    
+    #TODO: search through the whole email and find the right Ticket ID.
+
+    my ($From, $junk) = ParseSenderAddressFromHead($head);
+    
+    if (($From =~ /^mailer-daemon/i) or
+       ($From =~ /^postmaster/i)){
+       return (1);
+       
+    }
+    
+    return (undef);
+
+}
+
+# }}}
+
+# {{{ sub CheckForAutoGenerated
+sub CheckForAutoGenerated {
+    my $head = shift;
+    
+    my $Precedence = $head->get("Precedence") || "" ;
+    if ($Precedence =~ /^(bulk|junk)/i) {
+       return (1);
+    }
+    else {
+       return (0);
+    }
+}
+
+# }}}
+
+# {{{ sub ParseMIMEEntityFromSTDIN
+
+sub ParseMIMEEntityFromSTDIN {
+
+    # Create a new parser object:
+    
+    my $parser = new MIME::Parser;
+    
+    # {{{ Config $parser to store large attacments in temp dir
+
+    ## TODO: Does it make sense storing to disk at all?  After all, we
+    ## need to put each msg as an in-core scalar before saving it to
+    ## the database, don't we?
+
+    ## At the same time, we should make sure that we nuke attachments 
+    ## Over max size and return them
+
+    ## TODO: Remove the temp dir when we don't need it any more.
+
+    my $AttachmentDir = File::Temp::tempdir (TMPDIR => 1, CLEANUP => 1);
+    
+    # Set up output directory for files:
+    $parser->output_dir("$AttachmentDir");
+  
+    #If someone includes a message, don't extract it
+    $parser->extract_nested_messages(0);
+
+   
+    # Set up the prefix for files with auto-generated names:
+    $parser->output_prefix("part");
+
+    # If content length is <= 20000 bytes, store each msg as in-core scalar;
+    # Else, write to a disk file (the default action):
+  
+    $parser->output_to_core(20000);
+
+    # }}} (temporary directory)
+
+    #Ok. now that we're set up, let's get the stdin.
+    my $entity;
+    unless ($entity = $parser->read(\*STDIN)) {
+       die "couldn't parse MIME stream";
+    }
+    #Now we've got a parsed mime object. 
+    
+    # Get the head, a MIME::Head:
+    my $head = $entity->head;
+   
+
+    # Unfold headers that are have embedded newlines
+    $head->unfold; 
+    
+    # TODO - information about the charset is lost here!
+    $head->decode;
+
+    return ($entity, $head);
+
+}
+# }}}
+
+# {{{ sub ParseTicketId 
+
+sub ParseTicketId {
+    my $Subject = shift;
+    my ($Id);
+    
+    if ($Subject =~ s/\[$RT::rtname \#(\d+)\]//i) {
+       $Id = $1;
+       $RT::Logger->debug("Found a ticket ID. It's $Id");
+       return($Id);
+    }
+    else {
+       return(undef);
+    }
+}
+# }}}
+
+# {{{ sub MailError 
+sub MailError {
+    my %args = (To => $RT::OwnerEmail,
+               Bcc => undef,
+               From => $RT::CorrespondAddress,
+               Subject => 'There has been an error',
+               Explanation => 'Unexplained error',
+               MIMEObj => undef,
+               LogLevel => 'crit',
+               @_);
+
+
+    $RT::Logger->log(level => $args{'LogLevel'}, 
+                    message => $args{'Explanation'}
+                   );
+    my $entity = MIME::Entity->build( Type  =>"multipart/mixed",
+                                     From => $args{'From'},
+                                     Bcc => $args{'Bcc'},
+                                     To => $args{'To'},
+                                     Subject => $args{'Subject'},
+                                     'X-RT-Loop-Prevention' => $RT::rtname,
+                                   );
+
+    $entity->attach(  Data => $args{'Explanation'}."\n");
+    
+    my $mimeobj = $args{'MIMEObj'};
+    if ($mimeobj) {
+        $mimeobj->sync_headers();
+        $entity->add_part($mimeobj);
+    } 
+
+    if ($RT::MailCommand eq 'sendmailpipe') {
+        open (MAIL, "|$RT::SendmailPath $RT::SendmailArguments") || return(0);
+        print MAIL $entity->as_string;
+        close(MAIL);
+    }
+    else {
+       $entity->send($RT::MailCommand, $RT::MailParams);
+    }
+}
+
+# }}}
+
+# {{{ sub GetCurrentUser 
+
+sub GetCurrentUser  {
+    my $head = shift;
+    my $entity = shift;
+    my $ErrorsTo = shift;
+
+    my %UserInfo = ();
+
+    #Suck the address of the sender out of the header
+    my ($Address, $Name) = ParseSenderAddressFromHead($head);
+    
+    #This will apply local address canonicalization rules
+    $Address = RT::CanonicalizeAddress($Address);
+  
+    #If desired, synchronize with an external database
+
+    my $UserFoundInExternalDatabase = 0;
+
+    # Username is the 'Name' attribute of the user that RT uses for things
+    # like authentication
+    my $Username = undef;
+    if ($RT::LookupSenderInExternalDatabase) {
+       ($UserFoundInExternalDatabase, %UserInfo) = 
+         RT::LookupExternalUserInfo($Address, $Name);
+   
+       $Address = $UserInfo{'EmailAddress'};
+       $Username = $UserInfo{'Name'}; 
+    }
+    
+    my $CurrentUser = RT::CurrentUser->new();
+    
+    # First try looking up by a username, if we got one from the external
+    # db lookup. Next, try looking up by email address. Failing that,
+    # try looking up by users who have this user's email address as their
+    # username.
+
+    if ($Username) {
+       $CurrentUser->LoadByName($Username);
+    }  
+    
+    unless ($CurrentUser->Id) {
+       $CurrentUser->LoadByEmail($Address);
+    }  
+
+    #If we can't get it by email address, try by name.  
+    unless ($CurrentUser->Id) {
+       $CurrentUser->LoadByName($Address);
+    }
+    
+    
+    unless ($CurrentUser->Id) {
+        #If we couldn't load a user, determine whether to create a user
+
+        # {{{ If we require an incoming address to be found in the external
+       # user database, reject the incoming message appropriately
+        if ( $RT::LookupSenderInExternalDatabase &&
+            $RT::SenderMustExistInExternalDatabase && 
+            !$UserFoundInExternalDatabase) {
+           
+           my $Message = "Sender's email address was not found in the user database.";
+
+           # {{{  This code useful only if you've defined an AutoRejectRequest template
+           
+           require RT::Template;
+           my $template = new RT::Template($RT::Nobody);
+           $template->Load('AutoRejectRequest');
+           $Message = $template->Content || $Message;
+           
+           # }}}
+           
+           MailError( To => $ErrorsTo,
+                      Subject => "Ticket Creation failed: user could not be created",
+                      Explanation => $Message,
+                      MIMEObj => $entity,
+                      LogLevel => 'notice'
+                    );
+
+           return($CurrentUser);
+
+       } 
+       # }}}
+       
+       else {
+           my $NewUser = RT::User->new($RT::SystemUser);
+           
+           my ($Val, $Message) = 
+             $NewUser->Create(Name => ($Username || $Address),
+                              EmailAddress => $Address,
+                              RealName => "$Name",
+                              Password => undef,
+                              Privileged => 0,
+                              Comments => 'Autocreated on ticket submission'
+                             );
+           
+           unless ($Val) {
+               
+               # Deal with the race condition of two account creations at once
+               #
+               if ($Username) {
+                   $NewUser->LoadByName($Username);
+               }
+               
+               unless ($NewUser->Id) {
+                   $NewUser->LoadByEmail($Address);
+               }
+               
+               unless ($NewUser->Id) {  
+                   MailError( To => $ErrorsTo,
+                              Subject => "User could not be created",
+                              Explanation => "User creation failed in mailgateway: $Message",
+                              MIMEObj => $entity,
+                              LogLevel => 'crit'
+                            );
+               }
+           }
+       }
+       
+       #Load the new user object
+       $CurrentUser->LoadByEmail($Address);
+       
+       unless ($CurrentUser->id) {
+           $RT::Logger->warning("Couldn't load user '$Address'.".  "giving up");
+               MailError( To => $ErrorsTo,
+                          Subject => "User could not be loaded",
+                          Explanation => "User  '$Address' could not be loaded in the mail gateway",
+                          MIMEObj => $entity,
+                          LogLevel => 'crit'
+                        );
+           
+       }
+    }
+    
+    return ($CurrentUser);
+    
+}
+# }}}
+
+# {{{ ParseCcAddressesFromHead 
+
+=head2 ParseCcAddressesFromHead HASHREF
+
+Takes a hashref object containing QueueObj, Head and CurrentUser objects.
+Returns a list of all email addresses in the To and Cc 
+headers b<except> the current Queue\'s email addresses, the CurrentUser\'s 
+email address  and anything that the configuration sub RT::IsRTAddress matches.
+
+=cut
+  
+sub ParseCcAddressesFromHead {
+    my %args = ( Head => undef,
+                QueueObj => undef,
+                CurrentUser => undef,
+                @_ );
+    
+    my (@Addresses);
+        
+    my @ToObjs = Mail::Address->parse($args{'Head'}->get('To'));
+    my @CcObjs = Mail::Address->parse($args{'Head'}->get('Cc'));
+    
+    foreach my $AddrObj (@ToObjs, @CcObjs) {
+       my $Address = $AddrObj->address;
+       $Address = RT::CanonicalizeAddress($Address);
+       next if ($args{'CurrentUser'}->EmailAddress =~ /^$Address$/i);
+       next if ($args{'QueueObj'}->CorrespondAddress =~ /^$Address$/i);
+       next if ($args{'QueueObj'}->CommentAddress =~ /^$Address$/i);
+       next if (RT::IsRTAddress($Address));
+       
+       push (@Addresses, $Address);
+    }
+    return (@Addresses);
+}
+
+
+# }}}
+
+# {{{ ParseSenderAdddressFromHead
+
+=head2 ParseSenderAddressFromHead
+
+Takes a MIME::Header object. Returns a tuple: (user@host, friendly name) 
+of the From (evaluated in order of Reply-To:, From:, Sender)
+
+=cut
+
+sub ParseSenderAddressFromHead {
+    my $head = shift;
+    #Figure out who's sending this message.
+    my $From = $head->get('Reply-To') || 
+      $head->get('From') || 
+       $head->get('Sender');
+    return (ParseAddressFromHeader($From));
+}
+# }}}
+
+# {{{ ParseErrorsToAdddressFromHead
+
+=head2 ParseErrorsToAddressFromHead
+
+Takes a MIME::Header object. Return a single value : user@host
+of the From (evaluated in order of Errors-To:,Reply-To:, From:, Sender)
+
+=cut
+
+sub ParseErrorsToAddressFromHead {
+    my $head = shift;
+    #Figure out who's sending this message.
+
+    foreach my $header ('Errors-To' , 'Reply-To', 'From', 'Sender' ) {
+       # If there's a header of that name
+       my $headerobj = $head->get($header);
+       if ($headerobj) {
+               my ($addr, $name ) = ParseAddressFromHeader($headerobj);
+               # If it's got actual useful content...
+               return ($addr) if ($addr);
+       }
+    }
+}
+# }}}
+
+# {{{ ParseAddressFromHeader
+
+=head2 ParseAddressFromHeader ADDRESS
+
+Takes an address from $head->get('Line') and returns a tuple: user@host, friendly name
+
+=cut
+
+
+sub ParseAddressFromHeader{
+    my $Addr = shift;
+    
+    my @Addresses = Mail::Address->parse($Addr);
+    
+    my $AddrObj = $Addresses[0];
+
+    unless (ref($AddrObj)) {
+       return(undef,undef);
+    }
+    my $Name =  ($AddrObj->phrase || $AddrObj->comment || $AddrObj->address);
+
+
+    #Lets take the from and load a user object.
+    my $Address = $AddrObj->address;
+
+    return ($Address, $Name);
+}
+# }}}
+
+
+1;
diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm
new file mode 100644 (file)
index 0000000..6b52728
--- /dev/null
@@ -0,0 +1,1287 @@
+## $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Interface/Web.pm,v 1.1 2002-08-12 06:17:08 ivan Exp $
+
+## Portions Copyright 2000 Tobias Brox <tobix@fsck.com>
+## Copyright 1996-2002 Jesse Vincent <jesse@bestpractical.com>
+
+## This is a library of static subs to be used by the Mason web
+## interface to RT
+
+package RT::Interface::Web;
+
+# {{{ sub NewParser
+
+=head2 NewParser
+
+  Returns a new Mason::Parser object. Takes a param hash of things 
+  that get passed to HTML::Mason::Parser. Currently hard coded to only
+  take the parameter 'allow_globals'.
+
+=cut
+
+sub NewParser {
+    my %args = (
+        allow_globals => undef,
+        @_
+    );
+
+    my $parser = new HTML::Mason::Parser(
+        default_escape_flags => 'h',
+        allow_globals        => $args{'allow_globals'}
+    );
+    return ($parser);
+}
+
+# }}}
+
+# {{{ sub NewInterp
+
+=head2 NewInterp 
+
+  Takes a paremeter hash. Needs a param called 'parser' which is a reference
+  to an HTML::Mason::Parser.
+  returns a new Mason::Interp object
+
+=cut
+
+sub NewInterp {
+    my %params = (
+        comp_root                    => [
+            [ local    => $RT::MasonLocalComponentRoot ],
+            [ standard => $RT::MasonComponentRoot ]
+        ],
+        data_dir => "$RT::MasonDataDir",
+        @_
+    );
+
+    #We allow recursive autohandlers to allow for RT auth.
+
+    use HTML::Mason::Interp;
+    my $interp = new HTML::Mason::Interp(%params);
+
+}
+
+# }}}
+
+# {{{ sub NewApacheHandler 
+
+=head2 NewApacheHandler
+
+  Takes a Mason::Interp object
+  Returns a new Mason::ApacheHandler object
+
+=cut
+
+sub NewApacheHandler {
+    my $interp = shift;
+    my $ah = new HTML::Mason::ApacheHandler( interp => $interp );
+    return ($ah);
+}
+
+# }}}
+
+
+# {{{ sub NewMason11ApacheHandler
+
+=head2 NewMason11ApacheHandler
+
+  Returns a new Mason::ApacheHandler object
+
+=cut
+
+sub NewMason11ApacheHandler {
+        my %args = ( default_escape_flags => 'h',
+                    allow_globals        => [%session],
+        comp_root                    => [
+            [ local    => $RT::MasonLocalComponentRoot ],
+            [ standard => $RT::MasonComponentRoot ]
+        ],
+        data_dir => "$RT::MasonDataDir",
+        args_method => 'CGI'
+    );
+    my $ah = new HTML::Mason::ApacheHandler(%args);
+    return ($ah);
+}
+
+# }}}
+
+
+
+
+
+# }}}
+
+package HTML::Mason::Commands;
+
+# {{{ sub Abort
+# Error - calls Error and aborts
+sub Abort {
+
+    if ( $session{'ErrorDocument'} && $session{'ErrorDocumentType'} ) {
+        SetContentType( $session{'ErrorDocumentType'} );
+        $m->comp( $session{'ErrorDocument'}, Why => shift );
+        $m->abort;
+    }
+    else {
+        SetContentType('text/html');
+        $m->comp( "/Elements/Error", Why => shift );
+        $m->abort;
+    }
+}
+
+# }}}
+
+# {{{ sub CreateTicket 
+
+=head2 CreateTicket ARGS
+
+Create a new ticket, using Mason's %ARGS.  returns @results.
+=cut
+
+sub CreateTicket {
+    my %ARGS = (@_);
+
+    my (@Actions);
+
+    my $Ticket = new RT::Ticket( $session{'CurrentUser'} );
+
+    my $Queue = new RT::Queue( $session{'CurrentUser'} );
+    unless ( $Queue->Load( $ARGS{'Queue'} ) ) {
+        Abort('Queue not found');
+    }
+
+    unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
+        Abort('You have no permission to create tickets in that queue.');
+    }
+
+    my $due = new RT::Date( $session{'CurrentUser'} );
+    $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
+    my $starts = new RT::Date( $session{'CurrentUser'} );
+    $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
+
+    my @Requestors = split ( /,/, $ARGS{'Requestors'} );
+    my @Cc         = split ( /,/, $ARGS{'Cc'} );
+    my @AdminCc    = split ( /,/, $ARGS{'AdminCc'} );
+
+    my $MIMEObj = MakeMIMEEntity(
+        Subject             => $ARGS{'Subject'},
+        From                => $ARGS{'From'},
+        Cc                  => $ARGS{'Cc'},
+        Body                => $ARGS{'Content'},
+        AttachmentFieldName => 'Attach'
+    );
+
+    my %create_args = (
+        Queue           => $ARGS{Queue},
+        Owner           => $ARGS{Owner},
+        InitialPriority => $ARGS{InitialPriority},
+        FinalPriority   => $ARGS{FinalPriority},
+        TimeLeft        => $ARGS{TimeLeft},
+        TimeWorked      => $ARGS{TimeWorked},
+        Requestor       => \@Requestors,
+        Cc              => \@Cc,
+        AdminCc         => \@AdminCc,
+        Subject         => $ARGS{Subject},
+        Status          => $ARGS{Status},
+        Due             => $due->ISO,
+        Starts          => $starts->ISO,
+        MIMEObj         => $MIMEObj
+    );
+
+    # we need to get any KeywordSelect-<integer> fields into %create_args..
+    grep { $_ =~ /^KeywordSelect-/ &&{ $create_args{$_} = $ARGS{$_} } } %ARGS;
+
+    my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
+    unless ( $id && $Trans ) {
+        Abort($ErrMsg);
+    }
+    my @linktypes = qw( DependsOn MemberOf RefersTo );
+
+    foreach my $linktype (@linktypes) {
+        foreach my $luri ( split ( / /, $ARGS{"new-$linktype"} ) ) {
+            $luri =~ s/\s*$//;    # Strip trailing whitespace
+            my ( $val, $msg ) = $Ticket->AddLink(
+                Target => $luri,
+                Type   => $linktype
+            );
+            push ( @Actions, $msg ) unless ($val);
+        }
+
+        foreach my $luri ( split ( / /, $ARGS{"$linktype-new"} ) ) {
+            my ( $val, $msg ) = $Ticket->AddLink(
+                Base => $luri,
+                Type => $linktype
+            );
+
+            push ( @Actions, $msg ) unless ($val);
+        }
+    }
+
+    push ( @Actions, $ErrMsg );
+    unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
+        Abort( "No permission to view newly created ticket #"
+            . $Ticket->id . "." );
+    }
+    return ( $Ticket, @Actions );
+
+}
+
+# }}}
+
+# {{{ sub LoadTicket - loads a ticket
+
+=head2  LoadTicket id
+
+Takes a ticket id as its only variable. if it's handed an array, it takes
+the first value.
+
+Returns an RT::Ticket object as the current user.
+
+=cut
+
+sub LoadTicket {
+    my $id = shift;
+
+    if ( ref($id) eq "ARRAY" ) {
+        $id = $id->[0];
+    }
+
+    unless ($id) {
+        Abort("No ticket specified");
+    }
+
+    my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
+    $Ticket->Load($id);
+    unless ( $Ticket->id ) {
+        Abort("Could not load ticket $id");
+    }
+    return $Ticket;
+}
+
+# }}}
+
+# {{{ sub ProcessUpdateMessage
+
+sub ProcessUpdateMessage {
+
+    #TODO document what else this takes.
+    my %args = (
+        ARGSRef   => undef,
+        Actions   => undef,
+        TicketObj => undef,
+        @_
+    );
+
+    #Make the update content have no 'weird' newlines in it
+    if ( $args{ARGSRef}->{'UpdateContent'} ) {
+
+        if (
+            $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject() )
+        {
+            $args{ARGSRef}->{'UpdateSubject'} = undef;
+        }
+
+        my $Message = MakeMIMEEntity(
+            Subject             => $args{ARGSRef}->{'UpdateSubject'},
+            Body                => $args{ARGSRef}->{'UpdateContent'},
+            AttachmentFieldName => 'UpdateAttachment'
+        );
+
+       ## Check whether this was a refresh or not.  
+
+       # Match Correspondence or Comments.
+        my $trans_flag = -2;
+       my $trans_type = undef;
+       my $orig_trans = $args{ARGSRef}->{'UpdateType'};
+        if ( $orig_trans =~ /^(private|public)$/ ) {
+           $trans_type = "Comment";
+        }elsif ( $orig_trans eq 'response' ) {
+           $trans_type = "Correspond";
+       }
+
+       # Do we have a transaction that we need to update on? session
+       if( defined( $trans_type ) ){
+           $trans_flag = 0;
+
+           # Prepare a checksum.
+           # See perldoc -f unpack for example of this.
+           my $this_checksum = unpack("%32C*", $Message->body_as_string ) % 65535;
+
+           # The above *could* generate duplicate checksums.  Crosscheck with
+           # the length.
+           my $this_length = length( $Message->body_as_string );
+
+           # Don't forget the ticket id.
+           my $this_id = $args{TicketObj}->id;
+
+           # Check whether the previous transaction in the
+           # ticket is the same as the current transaction.
+           if( defined( $session{'prev_trans_type'} ) && defined( $session{'prev_trans_chksum'} ) && defined( $session{'prev_trans_length'} ) && defined( $session{'prev_trans_tickid'} ) ){
+               if( $session{'prev_trans_type'} eq $orig_trans && $session{'prev_trans_chksum'} == $this_checksum && $session{'prev_trans_length'} == $this_length && $session{'prev_trans_tickid'} == $this_id ){
+                   # Its the same as the previous transaction for this user.
+                   $trans_flag = -1;
+               }
+           }
+
+           # Store them for next time.
+           $session{'prev_trans_type'} = $orig_trans;
+           $session{'prev_trans_chksum'} = $this_checksum;
+           $session{'prev_trans_length'} = $this_length;
+           $session{'prev_trans_tickid'} = $this_id;
+
+           if( $trans_flag == -1 ){
+                push ( @{ $args{'Actions'} },
+"This appears to be a duplicate of your previous update (please do not refresh this page)" );
+           }
+
+
+            if ( $trans_type eq 'Comment' && $trans_flag >= 0 ) {
+                my ( $Transaction, $Description ) = $args{TicketObj}->Comment(
+                    CcMessageTo  => $args{ARGSRef}->{'UpdateCc'},
+                    BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
+                    MIMEObj      => $Message,
+                    TimeTaken    => $args{ARGSRef}->{'UpdateTimeWorked'}
+                );
+                push ( @{ $args{Actions} }, $Description );
+            }
+            elsif ( $trans_type eq 'Correspond' && $trans_flag >= 0 ) {
+                my ( $Transaction, $Description ) = $args{TicketObj}->Correspond(
+                    CcMessageTo  => $args{ARGSRef}->{'UpdateCc'},
+                    BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
+                    MIMEObj      => $Message,
+                    TimeTaken    => $args{ARGSRef}->{'UpdateTimeWorked'}
+                );
+                push ( @{ $args{Actions} }, $Description );
+            }
+       }
+        else {
+            push ( @{ $args{'Actions'} },
+    "Update type was neither correspondence nor comment. Update not recorded"
+                );
+        }
+    }
+}
+
+# }}}
+
+# {{{ sub MakeMIMEEntity
+
+=head2 MakeMIMEEntity PARAMHASH
+
+Takes a paramhash Subject, Body and AttachmentFieldName.
+
+  Returns a MIME::Entity.
+
+=cut
+
+sub MakeMIMEEntity {
+
+    #TODO document what else this takes.
+    my %args = (
+        Subject             => undef,
+        From                => undef,
+        Cc                  => undef,
+        Body                => undef,
+        AttachmentFieldName => undef,
+        @_
+    );
+
+    #Make the update content have no 'weird' newlines in it
+
+    $args{'Body'} =~ s/\r\n/\n/gs;
+    my $Message = MIME::Entity->build(
+        Subject => $args{'Subject'} || "",
+        From    => $args{'From'},
+        Cc      => $args{'Cc'},
+        Data    => [ $args{'Body'} ]
+    );
+
+    my $cgi_object = CGIObject();
+    if ( $cgi_object->param( $args{'AttachmentFieldName'} ) ) {
+
+        my $cgi_filehandle =
+          $cgi_object->upload( $args{'AttachmentFieldName'} );
+
+        use File::Temp qw(tempfile tempdir);
+
+        #foreach my $filehandle (@filenames) {
+
+        # my ( $fh, $temp_file ) = tempfile();
+
+        #$binmode $fh;    #thank you, windows
+
+        # We're having trouble with tempfiles not getting created. Let's try it with 
+        # a scalar instead
+
+        my ( $buffer, @file );
+
+        while ( my $bytesread = read( $cgi_filehandle, $buffer, 4096 ) ) {
+            push ( @file, $buffer );
+        }
+
+        $RT::Logger->debug($file);
+        my $filename = "$cgi_filehandle";
+        $filename =~ s#^(.*)/##;
+        $filename =~ s#^(.*)\\##;
+        my $uploadinfo = $cgi_object->uploadInfo($cgi_filehandle);
+        $Message->attach(
+            Data => \@file,
+
+            #Path     => $temp_file,
+            Filename => $filename,
+            Type     => $uploadinfo->{'Content-Type'}
+        );
+
+        #close($fh);
+        #unlink($temp_file);
+
+        #      }
+    }
+    $Message->make_singlepart();
+    return ($Message);
+
+}
+
+# }}}
+
+# {{{ sub ProcessSearchQuery
+
+=head2 ProcessSearchQuery
+
+  Takes a form such as the one filled out in webrt/Search/Elements/PickRestriction and turns it into something that RT::Tickets can understand.
+
+TODO Doc exactly what comes in the paramhash
+
+
+=cut
+
+sub ProcessSearchQuery {
+    my %args = @_;
+
+    ## TODO: The only parameter here is %ARGS.  Maybe it would be
+    ## cleaner to load this parameter as $ARGS, and use $ARGS->{...}
+    ## instead of $args{ARGS}->{...} ? :)
+
+    #Searches are sticky.
+    if ( defined $session{'tickets'} ) {
+
+        # Reset the old search
+        $session{'tickets'}->GotoFirstItem;
+    }
+    else {
+
+        # Init a new search
+        $session{'tickets'} = RT::Tickets->new( $session{'CurrentUser'} );
+    }
+
+    #Import a bookmarked search if we have one
+    if ( defined $args{ARGS}->{'Bookmark'} ) {
+        $session{'tickets'}->ThawLimits( $args{ARGS}->{'Bookmark'} );
+    }
+
+    # {{{ Goto next/prev page
+    if ( $args{ARGS}->{'GotoPage'} eq 'Next' ) {
+        $session{'tickets'}->NextPage;
+    }
+    elsif ( $args{ARGS}->{'GotoPage'} eq 'Prev' ) {
+        $session{'tickets'}->PrevPage;
+    }
+
+    # }}}
+
+    # {{{ Deal with limiting the search
+
+    if ( $args{ARGS}->{'RefreshSearchInterval'} ) {
+        $session{'tickets_refresh_interval'} =
+          $args{ARGS}->{'RefreshSearchInterval'};
+    }
+
+    if ( $args{ARGS}->{'TicketsSortBy'} ) {
+        $session{'tickets_sort_by'}    = $args{ARGS}->{'TicketsSortBy'};
+        $session{'tickets_sort_order'} = $args{ARGS}->{'TicketsSortOrder'};
+        $session{'tickets'}->OrderBy(
+            FIELD => $args{ARGS}->{'TicketsSortBy'},
+            ORDER => $args{ARGS}->{'TicketsSortOrder'}
+        );
+    }
+
+    # }}}
+
+    # {{{ Set the query limit
+    if ( defined $args{ARGS}->{'RowsPerPage'} ) {
+        $RT::Logger->debug(
+            "limiting to " . $args{ARGS}->{'RowsPerPage'} . " rows" );
+
+        $session{'tickets_rows_per_page'} = $args{ARGS}->{'RowsPerPage'};
+        $session{'tickets'}->RowsPerPage( $args{ARGS}->{'RowsPerPage'} );
+    }
+
+    # }}}
+    # {{{ Limit priority
+    if ( $args{ARGS}->{'ValueOfPriority'} ne '' ) {
+        $session{'tickets'}->LimitPriority(
+            VALUE    => $args{ARGS}->{'ValueOfPriority'},
+            OPERATOR => $args{ARGS}->{'PriorityOp'}
+        );
+    }
+
+    # }}}
+    # {{{ Limit owner
+    if ( $args{ARGS}->{'ValueOfOwner'} ne '' ) {
+        $session{'tickets'}->LimitOwner(
+            VALUE    => $args{ARGS}->{'ValueOfOwner'},
+            OPERATOR => $args{ARGS}->{'OwnerOp'}
+        );
+    }
+
+    # }}}
+    # {{{ Limit requestor email
+
+    if ( $args{ARGS}->{'ValueOfRequestor'} ne '' ) {
+        my $alias = $session{'tickets'}->LimitRequestor(
+            VALUE    => $args{ARGS}->{'ValueOfRequestor'},
+            OPERATOR => $args{ARGS}->{'RequestorOp'},
+        );
+
+    }
+
+    # }}}
+    # {{{ Limit Queue
+    if ( $args{ARGS}->{'ValueOfQueue'} ne '' ) {
+        $session{'tickets'}->LimitQueue(
+            VALUE    => $args{ARGS}->{'ValueOfQueue'},
+            OPERATOR => $args{ARGS}->{'QueueOp'}
+        );
+    }
+
+    # }}}
+    # {{{ Limit Status
+    if ( $args{ARGS}->{'ValueOfStatus'} ne '' ) {
+        if ( ref( $args{ARGS}->{'ValueOfStatus'} ) ) {
+            foreach my $value ( @{ $args{ARGS}->{'ValueOfStatus'} } ) {
+                $session{'tickets'}->LimitStatus(
+                    VALUE    => $value,
+                    OPERATOR => $args{ARGS}->{'StatusOp'},
+                );
+            }
+        }
+        else {
+            $session{'tickets'}->LimitStatus(
+                VALUE    => $args{ARGS}->{'ValueOfStatus'},
+                OPERATOR => $args{ARGS}->{'StatusOp'},
+            );
+        }
+
+    }
+
+    # }}}
+    # {{{ Limit Subject
+    if ( $args{ARGS}->{'ValueOfSubject'} ne '' ) {
+        $session{'tickets'}->LimitSubject(
+            VALUE    => $args{ARGS}->{'ValueOfSubject'},
+            OPERATOR => $args{ARGS}->{'SubjectOp'},
+        );
+    }
+
+    # }}}    
+    # {{{ Limit Dates
+    if ( $args{ARGS}->{'ValueOfDate'} ne '' ) {
+
+        my $date = ParseDateToISO( $args{ARGS}->{'ValueOfDate'} );
+        $args{ARGS}->{'DateType'} =~ s/_Date$//;
+
+        $session{'tickets'}->LimitDate(
+            FIELD    => $args{ARGS}->{'DateType'},
+            VALUE    => $date,
+            OPERATOR => $args{ARGS}->{'DateOp'},
+        );
+    }
+
+    # }}}    
+    # {{{ Limit Content
+    if ( $args{ARGS}->{'ValueOfContent'} ne '' ) {
+        $session{'tickets'}->LimitContent(
+            VALUE    => $args{ARGS}->{'ValueOfContent'},
+            OPERATOR => $args{ARGS}->{'ContentOp'},
+        );
+    }
+
+    # }}}   
+    # {{{ Limit KeywordSelects
+
+    foreach my $KeywordSelectId (
+        map { /^KeywordSelect(\d+)$/; $1 }
+        grep { /^KeywordSelect(\d+)$/; } keys %{ $args{ARGS} }
+      )
+    {
+        my $form = $args{ARGS}->{"KeywordSelect$KeywordSelectId"};
+        my $oper = $args{ARGS}->{"KeywordSelectOp$KeywordSelectId"};
+        foreach my $KeywordId ( ref($form) ? @{$form} : ($form) ) {
+            next unless ($KeywordId);
+            my $quote = 1;
+            if ( $KeywordId =~ /^null$/i ) {
+
+                #Don't quote the string 'null'
+                $quote = 0;
+
+                # Convert the operator to something apropriate for nulls
+                $oper = 'IS'     if ( $oper eq '=' );
+                $oper = 'IS NOT' if ( $oper eq '!=' );
+            }
+            $session{'tickets'}->LimitKeyword(
+                KEYWORDSELECT => $KeywordSelectId,
+                OPERATOR      => $oper,
+                QUOTEVALUE    => $quote,
+                KEYWORD       => $KeywordId
+            );
+        }
+    }
+
+    # }}}
+
+}
+
+# }}}
+
+# {{{ sub ParseDateToISO
+
+=head2 ParseDateToISO
+
+Takes a date in an arbitrary format.
+Returns an ISO date and time in GMT
+
+=cut
+
+sub ParseDateToISO {
+    my $date = shift;
+
+    my $date_obj = new RT::Date($CurrentUser);
+    $date_obj->Set(
+        Format => 'unknown',
+        Value  => $date
+    );
+    return ( $date_obj->ISO );
+}
+
+# }}}
+
+# {{{ sub Config 
+# TODO: This might eventually read the cookies, user configuration
+# information from the DB, queue configuration information from the
+# DB, etc.
+
+sub Config {
+    my $args = shift;
+    my $key  = shift;
+    return $args->{$key} || $RT::WebOptions{$key};
+}
+
+# }}}
+
+# {{{ sub ProcessACLChanges
+
+sub ProcessACLChanges {
+    my $ACLref  = shift;
+    my $ARGSref = shift;
+
+    my @CheckACL = @$ACLref;
+    my %ARGS     = %$ARGSref;
+
+    my ( $ACL, @results );
+
+    # {{{ Add rights
+    foreach $ACL (@CheckACL) {
+        my ($Principal);
+
+        next unless ($ACL);
+
+        # Parse out what we're really talking about. 
+        if ( $ACL =~ /^(.*?)-(\d+)-(.*?)-(\d+)/ ) {
+            my $PrincipalType = $1;
+            my $PrincipalId   = $2;
+            my $Scope         = $3;
+            my $AppliesTo     = $4;
+
+            # {{{ Create an object called Principal
+            # so we can do rights operations
+
+            if ( $PrincipalType eq 'User' ) {
+                $Principal = new RT::User( $session{'CurrentUser'} );
+            }
+            elsif ( $PrincipalType eq 'Group' ) {
+                $Principal = new RT::Group( $session{'CurrentUser'} );
+            }
+            else {
+                Abort("$PrincipalType unknown principal type");
+            }
+
+            $Principal->Load($PrincipalId)
+              || Abort("$PrincipalType $PrincipalId couldn't be loaded");
+
+            # }}}
+
+            # {{{ load up an RT::ACL object with the same current vals of this ACL
+
+            my $CurrentACL = new RT::ACL( $session{'CurrentUser'} );
+            if ( $Scope eq 'Queue' ) {
+                $CurrentACL->LimitToQueue($AppliesTo);
+            }
+            elsif ( $Scope eq 'System' ) {
+                $CurrentACL->LimitToSystem();
+            }
+
+            $CurrentACL->LimitPrincipalToType($PrincipalType);
+            $CurrentACL->LimitPrincipalToId($PrincipalId);
+
+            # }}}
+
+            # {{{ Get the values of the select we're working with 
+            # into an array. it will contain all the new rights that have 
+            # been granted
+            #Hack to turn the ACL returned into an array
+            my @rights =
+              ref( $ARGS{"GrantACE-$ACL"} ) eq 'ARRAY'
+              ? @{ $ARGS{"GrantACE-$ACL"} }
+              : ( $ARGS{"GrantACE-$ACL"} );
+
+            # }}}
+
+            # {{{ Add any rights we need.
+
+            foreach my $right (@rights) {
+                next unless ($right);
+
+                #if the right that's been selected wasn't there before, add it.
+                unless (
+                    $CurrentACL->HasEntry(
+                        RightScope     => "$Scope",
+                        RightName      => "$right",
+                        RightAppliesTo => "$AppliesTo",
+                        PrincipalType  => $PrincipalType,
+                        PrincipalId    => $Principal->Id
+                    )
+                  )
+                {
+
+                    #Add new entry to list of rights.
+                    if ( $Scope eq 'Queue' ) {
+                        my $Queue = new RT::Queue( $session{'CurrentUser'} );
+                        $Queue->Load($AppliesTo);
+                        unless ( $Queue->id ) {
+                            Abort("Couldn't find a queue called $AppliesTo");
+                        }
+
+                        my ( $val, $msg ) = $Principal->GrantQueueRight(
+                            RightAppliesTo => $Queue->id,
+                            RightName      => "$right"
+                        );
+
+                        if ($val) {
+                            push ( @results,
+                                "Granted right $right to "
+                                  . $Principal->Name
+                                  . " for queue "
+                                  . $Queue->Name );
+                        }
+                        else {
+                            push ( @results, $msg );
+                        }
+                    }
+                    elsif ( $Scope eq 'System' ) {
+                        my ( $val, $msg ) = $Principal->GrantSystemRight(
+                            RightAppliesTo => $AppliesTo,
+                            RightName      => "$right"
+                        );
+                        if ($val) {
+                            push ( @results, "Granted system right '$right' to "
+                                  . $Principal->Name );
+                        }
+                        else {
+                            push ( @results, $msg );
+                        }
+                    }
+                }
+            }
+
+            # }}}
+        }
+    }
+
+    # }}} Add rights
+
+    # {{{ remove any rights that have been deleted
+
+    my @RevokeACE =
+      ref( $ARGS{"RevokeACE"} ) eq 'ARRAY' 
+      ? @{ $ARGS{"RevokeACE"} }
+      : ( $ARGS{"RevokeACE"} );
+
+    foreach my $aceid (@RevokeACE) {
+
+        my $right = new RT::ACE( $session{'CurrentUser'} );
+        $right->Load($aceid);
+        next unless ( $right->id );
+
+        my $phrase = "Revoked "
+          . $right->PrincipalType . " "
+          . $right->PrincipalObj->Name
+          . "'s right to "
+          . $right->RightName;
+
+        if ( $right->RightScope eq 'System' ) {
+            $phrase .= ' across all queues.';
+        }
+        else {
+            $phrase .= ' for the queue ' . $right->AppliesToObj->Name . '.';
+        }
+        my ( $val, $msg ) = $right->Delete();
+        if ($val) {
+            push ( @results, $phrase );
+        }
+        else {
+            push ( @results, $msg );
+        }
+    }
+
+    # }}}
+
+    return (@results);
+}
+
+# }}}
+
+# {{{ sub UpdateRecordObj
+
+=head2 UpdateRecordObj ( ARGSRef => \%ARGS, Object => RT::Record, AttributesRef => \@attribs)
+
+@attribs is a list of ticket fields to check and update if they differ from the  B<Object>'s current values. ARGSRef is a ref to HTML::Mason's %ARGS.
+
+Returns an array of success/failure messages
+
+=cut
+
+sub UpdateRecordObject {
+    my %args = (
+        ARGSRef       => undef,
+        AttributesRef => undef,
+        Object        => undef,
+        @_
+    );
+
+    my (@results);
+
+    my $object     = $args{'Object'};
+    my $attributes = $args{'AttributesRef'};
+    my $ARGSRef    = $args{'ARGSRef'};
+
+    foreach $attribute (@$attributes) {
+        if ( ( defined $ARGSRef->{"$attribute"} )
+            and ( $ARGSRef->{"$attribute"} ne $object->$attribute() ) )
+        {
+            $ARGSRef->{"$attribute"} =~ s/\r\n/\n/gs;
+
+            my $method = "Set$attribute";
+            my ( $code, $msg ) = $object->$method( $ARGSRef->{"$attribute"} );
+            push @results, "$attribute: $msg";
+        }
+    }
+    return (@results);
+}
+
+# }}}
+
+# {{{ sub ProcessTicketBasics
+
+=head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS );
+
+Returns an array of results messages.
+
+=cut
+
+sub ProcessTicketBasics {
+
+    my %args = (
+        TicketObj => undef,
+        ARGSRef   => undef,
+        @_
+    );
+
+    my $TicketObj = $args{'TicketObj'};
+    my $ARGSRef   = $args{'ARGSRef'};
+
+    # {{{ Set basic fields 
+    my @attribs = qw(
+      Subject
+      FinalPriority
+      Priority
+      TimeWorked
+      TimeLeft
+      Status
+      Queue
+    );
+
+    if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
+        my $tempqueue = RT::Queue->new($RT::SystemUser);
+        $tempqueue->Load( $ARGSRef->{'Queue'} );
+        if ( $tempqueue->id ) {
+            $ARGSRef->{'Queue'} = $tempqueue->Id();
+        }
+    }
+
+    my @results = UpdateRecordObject(
+        AttributesRef => \@attribs,
+        Object        => $TicketObj,
+        ARGSRef       => $ARGSRef
+    );
+
+    # We special case owner changing, so we can use ForceOwnerChange
+    if ( $ARGSRef->{'Owner'} && ( $TicketObj->Owner ne $ARGSRef->{'Owner'} ) ) {
+        my ($ChownType);
+        if ( $ARGSRef->{'ForceOwnerChange'} ) {
+            $ChownType = "Force";
+        }
+        else {
+            $ChownType = "Give";
+        }
+
+        my ( $val, $msg ) =
+          $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
+        push ( @results, "$msg" );
+    }
+
+    # }}}
+
+    return (@results);
+}
+
+# }}}
+
+# {{{ sub ProcessTicketWatchers
+
+=head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS );
+
+Returns an array of results messages.
+
+=cut
+
+sub ProcessTicketWatchers {
+    my %args = (
+        TicketObj => undef,
+        ARGSRef   => undef,
+        @_
+    );
+    my (@results);
+
+    my $Ticket  = $args{'TicketObj'};
+    my $ARGSRef = $args{'ARGSRef'};
+
+    # {{{ Munge watchers
+
+    foreach my $key ( keys %$ARGSRef ) {
+
+        # Delete deletable watchers
+        if ( ( $key =~ /^DelWatcher(\d*)$/ ) and ( $ARGSRef->{$key} ) ) {
+            my ( $code, $msg ) = $Ticket->DeleteWatcher($1);
+            push @results, $msg;
+        }
+
+        # Delete watchers in the simple style demanded by the bulk manipulator
+        elsif ( $key =~ /^Delete(Requestor|Cc|AdminCc)$/ ) {
+            my ( $code, $msg ) = $Ticket->DeleteWatcher( $ARGSRef->{$key}, $1 );
+            push @results, $msg;
+        }
+
+        # Add new wathchers by email address      
+        elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
+            and ( $key =~ /^WatcherTypeEmail(\d*)$/ ) )
+        {
+
+            #They're in this order because otherwise $1 gets clobbered :/
+            my ( $code, $msg ) = $Ticket->AddWatcher(
+                Type  => $ARGSRef->{$key},
+                Email => $ARGSRef->{ "WatcherAddressEmail" . $1 }
+            );
+            push @results, $msg;
+        }
+
+        #Add requestors in the simple style demanded by the bulk manipulator
+        elsif ( $key =~ /^Add(Requestor|Cc|AdminCc)$/ ) {
+            my ( $code, $msg ) = $Ticket->AddWatcher(
+                Type  => $1,
+                Email => $ARGSRef->{$key}
+            );
+            push @results, $msg;
+        }
+
+        # Add new  watchers by owner
+        elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
+            and ( $key =~ /^WatcherTypeUser(\d*)$/ ) )
+        {
+
+            #They're in this order because otherwise $1 gets clobbered :/
+            my ( $code, $msg ) =
+              $Ticket->AddWatcher( Type => $ARGSRef->{$key}, Owner => $1 );
+            push @results, $msg;
+        }
+    }
+
+    # }}}
+
+    return (@results);
+}
+
+# }}}
+
+# {{{ sub ProcessTicketDates
+
+=head2 ProcessTicketDates ( TicketObj => $Ticket, ARGSRef => \%ARGS );
+
+Returns an array of results messages.
+
+=cut
+
+sub ProcessTicketDates {
+    my %args = (
+        TicketObj => undef,
+        ARGSRef   => undef,
+        @_
+    );
+
+    my $Ticket  = $args{'TicketObj'};
+    my $ARGSRef = $args{'ARGSRef'};
+
+    my (@results);
+
+    # {{{ Set date fields
+    my @date_fields = qw(
+      Told
+      Resolved
+      Starts
+      Started
+      Due
+    );
+
+    #Run through each field in this list. update the value if apropriate
+    foreach $field (@date_fields) {
+        my ( $code, $msg );
+
+        my $DateObj = RT::Date->new( $session{'CurrentUser'} );
+
+        #If it's something other than just whitespace
+        if ( $ARGSRef->{ $field . '_Date' } ne '' ) {
+            $DateObj->Set(
+                Format => 'unknown',
+                Value  => $ARGSRef->{ $field . '_Date' }
+            );
+            my $obj = $field . "Obj";
+            if ( ( defined $DateObj->Unix )
+                and ( $DateObj->Unix ne $Ticket->$obj()->Unix() ) )
+            {
+                my $method = "Set$field";
+                my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
+                push @results, "$msg";
+            }
+        }
+    }
+
+    # }}}
+    return (@results);
+}
+
+# }}}
+
+# {{{ sub ProcessTicketLinks
+
+=head2 ProcessTicketLinks ( TicketObj => $Ticket, ARGSRef => \%ARGS );
+
+Returns an array of results messages.
+
+=cut
+
+sub ProcessTicketLinks {
+    my %args = (
+        TicketObj => undef,
+        ARGSRef   => undef,
+        @_
+    );
+
+    my $Ticket  = $args{'TicketObj'};
+    my $ARGSRef = $args{'ARGSRef'};
+
+    my (@results);
+
+    # Delete links that are gone gone gone.
+    foreach my $arg ( keys %$ARGSRef ) {
+        if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
+            my $base   = $1;
+            my $type   = $2;
+            my $target = $3;
+
+            push @results,
+              "Trying to delete: Base: $base Target: $target  Type $type";
+            my ( $val, $msg ) = $Ticket->DeleteLink(
+                Base   => $base,
+                Type   => $type,
+                Target => $target
+            );
+
+            push @results, $msg;
+
+        }
+
+    }
+
+    my @linktypes = qw( DependsOn MemberOf RefersTo );
+
+    foreach my $linktype (@linktypes) {
+
+        for my $luri ( split ( / /, $ARGSRef->{ $Ticket->Id . "-$linktype" } ) )
+        {
+            $luri =~ s/\s*$//;    # Strip trailing whitespace
+            my ( $val, $msg ) = $Ticket->AddLink(
+                Target => $luri,
+                Type   => $linktype
+            );
+            push @results, $msg;
+        }
+
+        for my $luri ( split ( / /, $ARGSRef->{ "$linktype-" . $Ticket->Id } ) )
+        {
+            my ( $val, $msg ) = $Ticket->AddLink(
+                Base => $luri,
+                Type => $linktype
+            );
+
+            push @results, $msg;
+        }
+    }
+
+    #Merge if we need to
+    if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
+        my ( $val, $msg ) =
+          $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
+        push @results, $msg;
+    }
+
+    return (@results);
+}
+
+# }}}
+
+# {{{ sub ProcessTicketObjectKeywords
+
+=head2 ProcessTicketObjectKeywords ( TicketObj => $Ticket, ARGSRef => \%ARGS );
+
+Returns an array of results messages.
+
+=cut
+
+sub ProcessTicketObjectKeywords {
+    my %args = (
+        TicketObj => undef,
+        ARGSRef   => undef,
+        @_
+    );
+
+    my $TicketObj = $args{'TicketObj'};
+    my $ARGSRef   = $args{'ARGSRef'};
+
+    my (@results);
+
+    # {{{ set ObjectKeywords.
+
+    my $KeywordSelects = $TicketObj->QueueObj->KeywordSelects;
+
+    # iterate through all the keyword selects for this queue
+    while ( my $KeywordSelect = $KeywordSelects->Next ) {
+
+        # {{{ do some setup
+
+        # if we have KeywordSelectMagic for this keywordselect:
+        next
+          unless
+          defined $ARGSRef->{ 'KeywordSelectMagic' . $KeywordSelect->id };
+
+        # Lets get a hash of the possible values to work with
+        my $value = $ARGSRef->{ 'KeywordSelect' . $KeywordSelect->id } || [];
+
+        #lets get all those values in a hash. regardless of # of entries
+        #we'll use this for adding and deleting keywords from this object.
+        my %values = map { $_ => 1 } ref($value) ? @{$value} : ($value);
+
+        # Load up the ObjectKeywords for this KeywordSelect for this ticket
+        my $ObjectKeys = $TicketObj->KeywordsObj( $KeywordSelect->id );
+
+        # }}}
+        # {{{ add new keywords
+
+        foreach my $key ( keys %values ) {
+
+            #unless the ticket has that keyword for that keyword select,
+            unless ( $ObjectKeys->HasEntry($key) ) {
+
+                #Add the keyword
+                my ( $result, $msg ) = $TicketObj->AddKeyword(
+                    Keyword       => $key,
+                    KeywordSelect => $KeywordSelect->id
+                );
+                push ( @results, $msg );
+            }
+        }
+
+        # }}}
+        # {{{ Delete unused keywords
+
+        #redo this search, so we don't ask it to delete things that are already gone
+        # such as when a single keyword select gets its value changed.
+        $ObjectKeys = $TicketObj->KeywordsObj( $KeywordSelect->id );
+
+        while ( my $TicketKey = $ObjectKeys->Next ) {
+
+            # if the hash defined above doesn\'t contain the keyword mentioned,
+            unless ( $values{ $TicketKey->Keyword } ) {
+
+                #I'd really love to just call $keyword->Delete, but then 
+                # we wouldn't get a transaction recorded
+                my ( $result, $msg ) = $TicketObj->DeleteKeyword(
+                    Keyword       => $TicketKey->Keyword,
+                    KeywordSelect => $KeywordSelect->id
+                );
+                push ( @results, $msg );
+            }
+        }
+
+        # }}}
+    }
+
+    #Iterate through the keyword selects for BulkManipulator style access
+    while ( my $KeywordSelect = $KeywordSelects->Next ) {
+        if ( $ARGSRef->{ "AddToKeywordSelect" . $KeywordSelect->Id } ) {
+
+            #Add the keyword
+            my ( $result, $msg ) = $TicketObj->AddKeyword(
+                Keyword =>
+                $ARGSRef->{ "AddToKeywordSelect" . $KeywordSelect->Id },
+                KeywordSelect => $KeywordSelect->id
+            );
+            push ( @results, $msg );
+        }
+        if ( $ARGSRef->{ "DeleteFromKeywordSelect" . $KeywordSelect->Id } ) {
+
+            #Delete the keyword
+            my ( $result, $msg ) = $TicketObj->DeleteKeyword(
+                Keyword =>
+                $ARGSRef->{ "DeleteFromKeywordSelect" . $KeywordSelect->Id },
+                KeywordSelect => $KeywordSelect->id
+            );
+            push ( @results, $msg );
+        }
+    }
+
+    # }}}
+
+    return (@results);
+}
+
+# }}}
+
+1;
diff --git a/rt/lib/RT/Keyword.pm b/rt/lib/RT/Keyword.pm
new file mode 100644 (file)
index 0000000..a41e0a5
--- /dev/null
@@ -0,0 +1,446 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attic/Keyword.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+ RT::Keyword - Manipulate an RT::Keyword record
+
+=head1 SYNOPSIS
+
+  use RT::Keyword;
+
+  my $keyword = RT::Keyword->new($CurrentUser);
+  $keyword->Create( Name => 'tofu',
+                   Description => 'fermented soy beans',
+                 );
+  
+
+  my $keyword2 = RT::Keyword->new($CurrentUser);
+  $keyword2->Create( Name   => 'beast',
+                   Description => 'a wild animal',
+                   Parent => $keyword->id(),
+                 );
+
+=head1 DESCRIPTION
+
+An B<RT::Keyword> object is an arbitrary string. 
+
+=head1 METHODS
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Scrip);
+
+=end testing
+
+
+=cut 
+package RT::Keyword;
+
+use strict;
+use vars qw(@ISA);
+use Tie::IxHash;
+use RT::Record;
+use RT::Keywords;
+
+@ISA = qw(RT::Record);
+
+# {{{ Core methods
+
+sub _Init {
+    my $self = shift;
+    $self->{'table'} = "Keywords";
+    $self->SUPER::_Init(@_);
+}
+
+sub _Accessible {
+    my $self = shift;
+    my %cols = (
+               Name        => 'read/write', #the keyword itself
+               Description => 'read/write', #a description of the keyword
+               Parent      => 'read/write', #optional id of another B<RT::Keyword>, allowing keywords to be arranged hierarchically
+               Disabled    => 'read/write'
+              );
+    return ($self->SUPER::_Accessible( @_, %cols));
+    
+}
+
+# }}}
+
+
+=over 4
+
+=item new CURRENT_USER
+
+Takes a single argument, an RT::CurrentUser object.  Instantiates a new
+(uncreated) RT::Keyword object.
+
+=cut
+
+# {{{ sub Create
+
+=item Create KEY => VALUE, ...
+
+Takes a list of key/value pairs and creates a the object.  Returns the id of
+the newly created record, or false if there was an error.
+
+Keys are:
+
+Name - the keyword itself
+Description - (not yet used)
+Parent - optional link to another B<RT::Keyword>, allowing keyword to be arranged in a hierarchical fashion.  Can be specified by id or Name.
+
+=cut
+
+sub Create {
+    my $self = shift;
+    my %args = (Name => undef,
+               Description => undef,
+               Parent => 0,
+               @_);
+    
+    unless ($self->CurrentUserHasRight('AdminKeywords')) {
+       return (0, 'Permission Denied');
+    }    
+  
+    if ( $args{'Parent'} && $args{'Parent'} !~ /^\d+$/ ) {
+       $RT::Logger->err( "can't yet specify parents by name, sorry: ". $args{'Parent'});
+       return(0,'Parent must be specified by id');
+    }
+    
+    my $val = $self->SUPER::Create(Name => $args{'Name'},
+                                  Description => $args{'Description'},
+                                  Parent => $args{'Parent'}
+                                 );
+    if ($val) {
+       return ($val, 'Keyword created');
+    }
+    else {
+       return(0,'Could not create keyword');
+    }  
+}
+
+# }}}
+
+# {{{ sub Delete
+
+sub Delete {
+    my $self = shift;
+    
+    return (0, 'Deleting this object would break referential integrity.');
+}
+
+# }}}
+
+# {{{ sub LoadByPath 
+
+=head2 LoadByPath STRING
+
+LoadByPath takes a string.  Whatever character starts the string is assumed to be a delimter.  The routine parses the keyword path description and tries to load the keyword
+described by that path.  It returns a numerical status and a textual message.
+A non-zero status means 'Success'.
+
+=cut
+
+sub LoadByPath {
+    my $self = shift;
+
+    my $path = shift;
+    
+    my $delimiter = substr($path,0,1);
+    my @path_elements = split($delimiter, $path);
+    
+    #throw awya the first bogus path element
+    shift @path_elements;
+    
+    my $parent = 0;
+    my ($tempkey);
+    #iterate through all the path elements loading up a
+    #keyword object. when we're done, this object becomes 
+    #whatever the last tempkey object was.
+    while (my $name = shift @path_elements) {
+       
+       $tempkey = new RT::Keyword($self->CurrentUser);
+
+       my $loaded = $tempkey->LoadByNameAndParentId($name, $parent);
+       
+       #Set the new parent for loading its child.
+       $parent = $tempkey->Id;
+       
+       #If the parent Id is 0, then we're not recursing through the tree
+       # time to bail
+       return (0, "Couldn't find keyword") unless ($tempkey->id());
+
+    }  
+    #Now that we're through with the loop, the last keyword loaded
+    # is the the one we wanted.
+    # we shouldn't need to explicitly load it like this. but we do. Thanks SQL
+    
+    $self->Load($tempkey->Id);
+    
+    return (1, 'Keyword loaded');
+}
+
+
+# }}}
+
+# {{{ sub LoadByNameAndParentId
+
+=head2 LoadByNameAndParentId NAME PARENT_ID
+  
+Takes two arguments, a keyword name and a parent id. Loads a keyword into 
+  the current object.
+
+=cut
+  
+sub LoadByNameAndParentId {
+    my $self = shift;
+    my $name = shift;
+    my $parentid = shift;
+    
+    my $val = $self->LoadByCols( Name => $name, Parent => $parentid);
+    if ($self->Id) {
+       return ($self->Id, 'Keyword loaded');
+    }  
+    else {
+       return (0, 'Keyword could not be found');
+    }
+  }
+
+# }}}
+
+
+# {{{ sub Load
+
+=head2 Load KEYWORD
+
+Loads KEYWORD, either by id if it's an integer or by Path, otherwise
+
+=cut
+
+sub Load {
+    my $self = shift;
+    my $id = shift;
+
+    if (!$id) {
+       return (0, 'No keyword defined');
+    }  
+    if ($id =~ /^(\d+)$/) {
+        return ($self->SUPER::Load($id));
+    }
+    else {
+        return($self->LoadByPath($id));
+    }
+}
+
+
+# }}}
+
+# {{{ sub Path
+
+=item Path
+
+  Returns this Keyword's full path going back to the root. (eg /OS/Unix/Linux/Redhat if 
+this keyword is "Redhat" )
+
+=cut
+
+sub Path {
+    my $self = shift;
+    
+    if ($self->Parent == 0) {
+       return ("/".$self->Name);
+    }
+    else {
+       return ( $self->ParentObj->Path . "/" . $self->Name);
+    }  
+    
+}
+
+# }}}
+
+# {{{ sub RelativePath 
+
+=head2 RelativePath KEYWORD_OBJ
+
+Takes a keyword object.  Returns this keyword's path relative to that
+keyword.  
+
+=item Bugs
+
+Currently assumes that the "other" keyword is a predecessor of this keyword
+
+=cut
+
+sub RelativePath {
+    my $self = shift;
+    my $OtherKey = shift;
+    
+    my $OtherPath = $OtherKey->Path();
+    my $MyPath = $self->Path;
+    $MyPath =~ s/^$OtherPath\///g;
+    return ($MyPath);
+}
+
+
+# }}}
+
+# {{{ sub ParentObj
+
+=item ParentObj
+
+  Returns an RT::Keyword object of this Keyword's 'parents'
+
+=cut
+
+sub ParentObj {
+    my $self = shift;
+    
+    my $ParentObj = new RT::Keyword($self->CurrentUser);
+    $ParentObj->Load($self->Parent);
+    return ($ParentObj);
+}
+
+# }}}
+
+# {{{ sub Children
+
+=item Children
+
+Return an RT::Keywords object  this Object's children.
+
+=cut
+
+sub Children {
+    my $self = shift;
+    
+    my $Children = new RT::Keywords($self->CurrentUser);
+    $Children->LimitToParent($self->id);
+    return ($Children);
+}
+
+# }}}
+
+# {{{ sub Descendents
+
+=item Descendents [ NUM_GENERATIONS [ EXCLUDE_HASHREF ]  ]
+
+Returns an ordered (see L<Tie::IxHash>) hash reference of the descendents of
+this keyword, possibly limited to a given number of generations.  The keys
+are B<RT::Keyword> I<id>s, and the values are strings containing the I<Name>s
+of those B<RT::Keyword>s.
+
+=cut
+
+sub Descendents {
+    my $self = shift;
+    my $generations = shift || 0;
+    my $exclude = shift || {};
+    my %results;
+    
+
+    tie %results, 'Tie::IxHash';
+    my $Keywords = new RT::Keywords($self->CurrentUser);
+    $Keywords->LimitToParent($self->id || 0 ); #If we have no id, start at the top
+    
+    while ( my $Keyword = $Keywords->Next ) {
+       
+       next if defined $exclude->{ $Keyword->id };
+       $results{ $Keyword->id } = $Keyword->Name;
+               
+       if ( $generations == 0 || $generations > 1 ) {
+           #if we're limiting to some number of generations,
+           # decrement the number of generations
+
+           my $nextgen = $generations;
+           $nextgen-- if ( $nextgen > 1 );
+           
+           my $kids = $Keyword->Descendents($nextgen, \%results);
+           
+           foreach my $kid ( keys %{$kids}) {
+               $results{"$kid"} = $Keyword->Name. "/". $kids->{"$kid"};
+           }
+       }
+    }
+    return(\%results);
+}
+
+# }}}
+
+# {{{ ACL related methods
+
+# {{{ sub _Set
+
+# does an acl check and then passes off the call
+sub _Set {
+    my $self = shift;
+    
+    unless ($self->CurrentUserHasRight('AdminKeywords')) {
+       return (0,'Permission Denied');
+    }
+    return $self->SUPER::_Set(@_);
+}
+
+# }}}
+
+# {{{ sub CurrentUserHasRight
+
+=head2 CurrentUserHasRight
+
+Helper menthod for HasRight. Presets Principal to CurrentUser then 
+calls HasRight.
+
+=cut
+
+sub CurrentUserHasRight {
+    my $self = shift;
+    my $right = shift;
+    return ($self->HasRight( Principal => $self->CurrentUser->UserObj,
+                             Right => $right ));
+    
+}
+
+# }}}
+
+# {{{ sub HasRight
+
+=head2 HasRight
+
+Takes a param-hash consisting of "Right" and "Principal"  Principal is 
+an RT::User object or an RT::CurrentUser object. "Right" is a textual
+Right string that applies to Keywords.
+
+=cut
+
+sub HasRight {
+    my $self = shift;
+    my %args = ( Right => undef,
+                 Principal => undef,
+                 @_ );
+
+    return( $args{'Principal'}->HasSystemRight( $args{'Right'}) );
+
+}
+# }}}
+
+# }}}
+
+=back
+
+=head1 AUTHOR
+
+Ivan Kohler <ivan-rt@420.am>
+
+=head1 BUGS
+
+Yes.
+
+=head1 SEE ALSO
+
+L<RT::Keywords>, L<RT::ObjectKeyword>, L<RT::ObjectKeywords>, L<RT::Ticket>,
+L<RT::Record>
+
+[A=cut
+
+1;
+
diff --git a/rt/lib/RT/KeywordSelect.pm b/rt/lib/RT/KeywordSelect.pm
new file mode 100644 (file)
index 0000000..6865216
--- /dev/null
@@ -0,0 +1,452 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attic/KeywordSelect.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+package RT::KeywordSelect;
+
+use strict;
+use vars qw(@ISA);
+use RT::Record;
+use RT::Keyword;
+
+@ISA = qw(RT::Record);
+
+# {{{ POD
+
+=head1 NAME
+
+ RT::KeywordSelect - Manipulate an RT::KeywordSelect record
+
+=head1 SYNOPSIS
+
+  use RT::KeywordSelect;
+
+  my $keyword_select = RT::KeywordSelect->new($CurrentUser);
+  $keyword_select->Create(
+    Keyword     => 20,
+    ObjectType => 'Ticket',
+    Name       => 'Choices'
+  );
+
+  my $keyword_select = RT::KeywordSelect->new($CurrentUser);
+  $keyword_select->Create(
+    Name        => 'Choices',                    
+    Keyword     => 20,
+    ObjectType  => 'Ticket',
+    ObjectField => 'Queue',
+    ObjectValue => 1,
+    Single      => 1,
+    Depth => 4,
+  );
+
+=head1 DESCRIPTION
+
+An B<RT::KeywordSelect> object is a link between a Keyword and a object
+type (one of: Ticket), titled by the I<Name> field of the B<RT::Keyword> such
+that:
+
+=over 4
+
+=item Object display will contain a field, titled with the I<Name> field and
+  showing any descendent keywords which are related to this object via the
+  B<RT::ObjectKeywords> table.
+
+=item Object creation for this object will contain a field titled with the
+  I<Name> field and containing the descendents of the B<RT::Keyword> as
+  choices.  If the I<Single> field of this B<RT::KeywordSelect> is true, each
+  object must be associated (via an B<RT::ObjectKeywords> record) to a single
+  descendent.  If the I<Single> field is false, each object may be connect to
+  zero, one, or many descendents.
+
+=item Searches for this object type will contain a selection field titled with
+  the I<Name> field and containing the descendents of the B<RT::Keyword> as
+  choices.
+
+=item If I<ObjectField> is defined (one of: Queue), all of the above apply only
+  when the value of I<ObjectField> (Queue) in B<ObjectType> (Ticket) matches
+  I<ObjectValue>.
+
+=back
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::KeywordSelects);
+
+=end testing
+
+
+=head1 METHODS
+
+
+=cut
+
+
+=over 4
+
+=item new CURRENT_USER
+
+Takes a single argument, an RT::CurrentUser object.  Instantiates a new
+(uncreated) RT::KeywordSelect object.
+
+=cut
+# }}}
+
+# {{{ sub _Init
+sub _Init {
+    my $self = shift;
+    $self->{'table'} = "KeywordSelects";
+    $self->SUPER::_Init(@_);
+}
+# }}}
+
+# {{{ sub _Accessible
+sub _Accessible {
+    my $self = shift;
+    my %Cols = (
+               Name => 'read/write',
+               Keyword => 'read/write', # link to Keywords.  Can be specified by id
+               Single => 'read/write', # bool (described below)
+
+               Depth => 'read/write', #- If non-zero, limits the descendents to this number of levels deep.
+               ObjectType  => 'read/write', # currently only C<Ticket>
+               ObjectField => 'read/write', #optional, currently only C<Queue>
+               ObjectValue => 'read/write', #constrains KeywordSelect function to when B<ObjectType>.I<ObjectField> equals I<ObjectValue>
+               Disabled => 'read/write'
+              );
+    return($self->SUPER::_Accessible(@_, %Cols));  
+}
+# }}}
+
+# {{{ sub LoadByName
+
+=head2 LoadByName( Name => [NAME], Queue => [QUEUE_ID])
+.  Takes a queue id and a keyword select name. 
+    tries to load the keyword select for that queue. if that fails, it tries to load it
+    without a queue specified.
+
+=cut
+
+
+sub LoadByName {
+    my $self = shift;
+    my %args = ( Name => undef,
+                Queue => undef,
+                @_
+              );
+    if ($args{'Queue'}) {
+       #Try to get the keyword select for this queue
+       $self->LoadByCols( Name => $args{'Name'}, 
+                          ObjectType => 'Ticket', 
+                          ObjectField => 'Queue', 
+                          ObjectValue => $args{'Queue'});
+    }  
+    unless ($self->Id) { #if that failed to load an object
+       #Try to get the keyword select of that name that's global
+       $self->LoadByCols( Name => $args{'Name'}, 
+                          ObjectType => 'Ticket', 
+                          ObjectField => 'Queue', 
+                          ObjectValue => '0');
+    }
+    
+    return($self->Id);
+    
+}
+
+# }}}
+
+# {{{ sub Create
+=item Create KEY => VALUE, ...
+
+Takes a list of key/value pairs and creates a the object.  Returns the id of
+the newly created record, or false if there was an error.
+
+Keys are:
+
+Keyword - link to Keywords.  Can be specified by id.
+Name - A name for this KeywordSelect
+Single - bool (described above)
+Depth - If non-zero, limits the descendents to this number of levels deep.
+ObjectType - currently only C<Ticket>
+ObjectField - optional, currently only C<Queue>
+ObjectValue - constrains KeywordSelect function to when B<ObjectType>.I<ObjectField> equals I<ObjectValue>
+
+=cut
+
+sub Create {
+    my $self = shift;
+    my %args = ( Keyword => undef,
+                Single => 1,
+                Depth => 0,
+                Name => undef,
+                ObjectType => undef,
+                ObjectField => undef,
+                ObjectValue => undef,
+                @_);
+
+    #If we're talking about a keyword select based on a ticket's 'Queue' field
+    if  ( ($args{'ObjectField'} eq 'Queue') and
+         ($args{'ObjectType'} eq 'Ticket')) {
+       
+       #If we're talking about a keywordselect for all queues
+       if ($args{'ObjectValue'} == 0) {
+           unless( $self->CurrentUserHasSystemRight('AdminKeywordSelects')) {
+               return (0, 'Permission Denied');
+           }
+       }  
+       #otherwise, we're talking about a keywordselect for a specific queue
+       else {
+           unless ($self->CurrentUserHasQueueRight( Right => 'AdminKeywordSelects',
+                                                    Queue => $args{'ObjectValue'})) {
+               return (0, 'Permission Denied');
+           }
+       }
+    }
+    else {
+       return (0, "Can't create a KeywordSelect for that object/field combo");
+    }
+
+    my $Keyword = new RT::Keyword($self->CurrentUser);
+
+    if ( $args{'Keyword'} && $args{'Keyword'} !~ /^\d+$/ ) {
+       $Keyword->LoadByPath($args{'Keyword'});
+    }  
+    else {
+       $Keyword->Load($args{'Keyword'});
+    }
+
+    unless ($Keyword->Id) {
+       $RT::Logger->debug("Keyword ".$args{'Keyword'} ." not found\n");
+       return(0, 'Keyword not found');
+    }
+    
+    $args{'Name'} = $Keyword->Name if  (!$args{'Name'});
+    
+    my $val = $self->SUPER::Create( Name => $args{'Name'},
+                                   Keyword => $Keyword->Id,
+                                   Single => $args{'Single'},
+                                   Depth => $args{'Depth'},
+                                   ObjectType => $args{'ObjectType'},
+                                   ObjectField => $args{'ObjectField'},
+                                   ObjectValue => $args{'ObjectValue'});
+    if ($val) {
+       return ($val, 'KeywordSelect Created');
+    }
+    else {
+       return (0, 'System error. KeywordSelect not created');
+       
+    }
+}
+# }}}
+
+# {{{ sub Delete
+
+sub Delete {
+    my $self = shift;
+    
+    return (0, 'Deleting this object would break referential integrity.');
+}
+
+# }}}
+
+
+# {{{ sub SetDisabled
+
+=head2 Sub SetDisabled
+
+Toggles the KeywordSelect's disabled flag.
+
+
+=cut 
+
+sub SetDisabled {
+    my $self = shift;
+    my $value = shift;
+
+    unless ($self->CurrentUserHasRight('AdminKeywordSelects')) {
+       return (0, "Permission Denied");
+    }
+    return($self->_Set(Field => 'Disabled', Value => $value));
+}
+
+# }}}
+
+# {{{ sub KeywordObj
+
+=item KeywordObj
+
+Returns the B<RT::Keyword> referenced by the I<Keyword> field.
+
+=cut
+
+sub KeywordObj {
+    my $self = shift;
+
+    my $Keyword = new RT::Keyword($self->CurrentUser);
+    $Keyword->Load( $self->Keyword ); #or ?
+    return($Keyword);
+} 
+# }}}
+
+# {{{ sub Object
+
+=item Object
+
+Returns the object (currently only RT::Queue) specified by ObjectField and ObjectValue.
+
+=cut
+
+sub Object {
+    my $self = shift;
+    if ( $self->ObjectField eq 'Queue' ) {
+       my $Queue = new RT::Queue($self->CurrentUser);
+       $Queue->Load( $self->ObjectValue );
+       return ($Queue);
+    } else {
+       $RT::Logger->error("$self trying to load an object value for a non-queue object");
+       return (undef);
+    }
+}
+
+# }}}
+
+# {{{ sub _Set
+
+# does an acl check, then passes off the call
+sub _Set {
+    my $self = shift;
+
+    unless ($self->CurrentUserHasRight('AdminKeywordSelects')) {
+       return (0, "Permission Denied");
+    }
+    
+    return ($self->SUPER::_Set(@_));
+
+}
+
+# }}}
+
+
+# {{{ sub CurrentUserHasQueueRight 
+
+=head2 CurrentUserHasQueueRight ( Queue => QUEUEID, Right => RIGHTNANAME )
+
+Check to see whether the current user has the specified right for the specified queue.
+
+=cut
+
+sub CurrentUserHasQueueRight {
+    my $self = shift;
+    my %args = (Queue => undef,
+               Right => undef,
+               @_
+               );
+    return ($self->HasRight( Right => $args{'Right'},
+                            Principal => $self->CurrentUser->UserObj,
+                            Queue => $args{'Queue'}));
+}
+
+# }}}
+
+# {{{ sub CurrentUserHasSystemRight 
+
+=head2 CurrentUserHasSystemRight RIGHTNAME
+
+Check to see whether the current user has the specified right for the 'system' scope.
+
+=cut
+
+sub CurrentUserHasSystemRight {
+    my $self = shift;
+    my $right = shift;
+    $RT::Logger->debug("$self in hashsysright for right $right\n");
+    return ($self->HasRight( Right => $right,
+                            System => 1,
+                            Principal => $self->CurrentUser->UserObj));
+}
+
+# }}}
+
+# {{{ sub CurrentUserHasRight
+
+=item CurrentUserHasRight RIGHT  [QUEUEID]
+
+Takes a rightname as a string. Can take a queue id as a second
+optional parameter, which can be useful to a routine like create.
+Helper menthod for HasRight. Presets Principal to CurrentUser then 
+calls HasRight.
+
+=cut
+
+sub CurrentUserHasRight {
+    my $self = shift;
+    my $right = shift;
+    return ($self->HasRight( Principal => $self->CurrentUser->UserObj,
+                             Right => $right,
+                          ));
+}
+
+# }}}
+
+# {{{ sub HasRight
+
+=item HasRight
+
+Takes a param-hash consisting of "Right" and "Principal"  Principal is 
+an RT::User object or an RT::CurrentUser object. "Right" is a textual
+Right string that applies to KeywordSelects
+
+=cut
+
+sub HasRight {
+    my $self = shift;
+    my %args = ( Right => undef,
+                 Principal => undef,
+                Queue => undef,
+                System => undef,
+                 @_ );
+
+    #If we're explicitly specifying a queue, as we need to do on create
+    if ($args{'Queue'}) {
+       return ($args{'Principal'}->HasQueueRight(Right => $args{'Right'},
+                                                 Queue => $args{'Queue'}));
+    }
+    #else if we're specifying to check a system right
+    elsif ($args{'System'}) {
+        return( $args{'Principal'}->HasSystemRight( $args{'Right'} ));
+    }  
+
+    #else if we 're using the object's queue
+    elsif (($self->__Value('ObjectField') eq 'Queue') and
+          ($self->__Value('ObjectValue') > 0 )) {
+        return ($args{'Principal'}->HasQueueRight(Right => $args{'Right'},
+                                                 Queue => $self->__Value('ObjectValue') )); 
+    }
+    
+    #If the object is system scoped.
+    else {
+        return( $args{'Principal'}->HasSystemRight( $args{'Right'} ));
+    }
+}
+
+# }}}
+
+=back
+
+=head1 AUTHORS
+
+Ivan Kohler <ivan-rt@420.am>, Jesse Vincent <jesse@fsck.com>
+
+=head1 BUGS
+
+The ACL system for this object is more byzantine than it should be.  reworking it eventually
+would be a good thing.
+
+=head1 SEE ALSO
+
+L<RT::KeywordSelects>, L<RT::Keyword>, L<RT::Keywords>, L<RT::ObjectKeyword>,
+L<RT::ObjectKeywords>, L<RT::Record>
+
+=cut
+
+1;
+
diff --git a/rt/lib/RT/KeywordSelects.pm b/rt/lib/RT/KeywordSelects.pm
new file mode 100644 (file)
index 0000000..c220b39
--- /dev/null
@@ -0,0 +1,143 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attic/KeywordSelects.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Scrip);
+
+=end testing
+
+=cut
+
+
+package RT::KeywordSelects;
+
+use strict;
+use vars qw( @ISA );
+use RT::EasySearch;
+use RT::KeywordSelect;
+
+@ISA = qw( RT::EasySearch );
+
+# {{{ _Init
+sub _Init {
+  my $self = shift;
+  $self->{'table'} = 'KeywordSelects';
+  $self->{'primary_key'} = 'id';
+  return ($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _DoSearch 
+
+=head2 _DoSearch
+
+  A subclass of DBIx::SearchBuilder::_DoSearch that makes sure that _Disabled rows never get seen unless
+we're explicitly trying to see them.
+
+=cut
+
+sub _DoSearch {
+    my $self = shift;
+    
+    #unless we really want to find disabled rows, make sure we\'re only finding enabled ones.
+    unless($self->{'find_disabled_rows'}) {
+       $self->LimitToEnabled();
+    }
+    
+    return($self->SUPER::_DoSearch(@_));
+    
+}
+
+# }}}
+
+# {{{ sub LimitToQueue
+=head2 LimitToQueue 
+
+Takes a queue id. Limits the returned set to KeywordSelects for that queue.
+Repeated calls will be OR'd together.
+
+=cut
+
+sub LimitToQueue {
+    my $self = shift;
+    my $queue = shift;
+
+    $self->Limit( FIELD => 'ObjectValue',
+                 VALUE => $queue,
+                 OPERATOR => '=',
+                 ENTRYAGGREGATOR => 'OR'
+               );
+
+    $self->Limit( FIELD => 'ObjectType',
+                 VALUE => 'Ticket',
+                 OPERATOR => '=');
+
+    $self->Limit( FIELD => 'ObjectField',
+                 VALUE => 'Queue',
+                 OPERATOR => '=');
+
+    
+}
+# }}}
+
+# {{{ sub LimitToGlobals
+
+=head2 LimitToGlobals
+
+Limits the returned set to KeywordSelects for all queues.
+Repeated calls will be OR'd together.
+
+=cut
+
+sub LimitToGlobals {
+    my $self = shift;
+
+    $self->Limit( FIELD => 'ObjectType',
+                 VALUE => 'Ticket',
+                 OPERATOR => '=');
+
+    $self->Limit( FIELD => 'ObjectField',
+                 VALUE => 'Queue',
+                 OPERATOR => '=');
+
+    $self->Limit( FIELD => 'ObjectValue',
+                 VALUE => '0',
+                 OPERATOR => '=',
+                 ENTRYAGGREGATOR => 'OR'
+               );
+    
+}
+# }}}
+
+# {{{ sub IncludeGlobals
+=head2 IncludeGlobals
+
+Include KeywordSelects which apply globally in the set of returned results
+
+=cut
+
+
+sub IncludeGlobals {
+    my $self = shift;
+    $self->Limit( FIELD => 'ObjectValue',
+                 VALUE => '0',
+                 OPERATOR => '=',
+                 ENTRYAGGREGATOR => 'OR'
+               );
+    
+
+}
+# }}}
+
+# {{{ sub NewItem
+sub NewItem {
+    my $self = shift;
+    #my $Handle = shift;
+    return (new RT::KeywordSelect($self->CurrentUser));
+}
+# }}}
+1;
+
diff --git a/rt/lib/RT/Keywords.pm b/rt/lib/RT/Keywords.pm
new file mode 100644 (file)
index 0000000..a9ecda2
--- /dev/null
@@ -0,0 +1,106 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attic/Keywords.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Keywords - a collection of RT::Keyword objects
+
+=head1 SYNOPSIS
+
+  use RT::Keywords;
+  my $keywords = RT::Keywords->new($user);
+  $keywords->LimitToParent(0);
+  while my ($keyword = $keywords->Next()) {
+     print $keyword->Name ."\n";
+  }
+
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Keywords);
+
+=end testing
+
+=cut
+
+package RT::Keywords;
+
+use strict;
+use vars qw( @ISA );
+use RT::EasySearch;
+use RT::Keyword;
+
+@ISA = qw( RT::EasySearch );
+
+
+# {{{ sub _Init
+
+sub _Init {
+    my $self = shift;
+    $self->{'table'} = 'Keywords';
+    $self->{'primary_key'} = 'id';
+
+    # By default, order by name
+    $self->OrderBy( ALIAS => 'main',
+                   FIELD => 'Name',
+                   ORDER => 'ASC');
+
+    return ($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _DoSearch 
+
+=head2 _DoSearch
+
+  A subclass of DBIx::SearchBuilder::_DoSearch that makes sure that _Disabled rows never get seen unless
+we're explicitly trying to see them.
+
+=cut
+
+sub _DoSearch {
+    my $self = shift;
+    
+    #unless we really want to find disabled rows, make sure we\'re only finding enabled ones.
+    unless($self->{'find_disabled_rows'}) {
+       $self->LimitToEnabled();
+    }
+    
+    return($self->SUPER::_DoSearch(@_));
+    
+}
+
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem {
+    my $self = shift;
+    return (RT::Keyword->new($self->CurrentUser));
+}
+# }}}
+
+# {{{ sub LimitToParent
+
+=head2 LimitToParent
+
+Takes a parent id and limits the returned keywords to children of that parent.
+
+=cut
+
+sub LimitToParent {
+    my $self = shift;
+    my $parent = shift;
+    $self->Limit( FIELD => 'Parent',
+                 VALUE => $parent,
+                 OPERATOR => '=',
+                 ENTRYAGGREGATOR => 'OR' );
+}      
+# }}}
+
+1;
+
diff --git a/rt/lib/RT/Link.pm b/rt/lib/RT/Link.pm
new file mode 100644 (file)
index 0000000..885ffe3
--- /dev/null
@@ -0,0 +1,373 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Link.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-1999 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+
+=head1 NAME
+
+  RT::Link - an RT Link object
+
+=head1 SYNOPSIS
+
+  use RT::Link;
+
+=head1 DESCRIPTION
+
+This module should never be called directly by client code. it's an internal module which
+should only be accessed through exported APIs in Ticket other similar objects.
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Link);
+
+=end testing
+
+=cut
+
+package RT::Link;
+use RT::Record;
+use Carp;
+@ISA= qw(RT::Record);
+
+# {{{ sub _Init
+sub _Init  {
+  my $self  = shift;
+  $self->{'table'} = "Links";
+  return ($self->SUPER::_Init(@_));
+}
+
+# }}}
+
+# {{{ sub Create 
+
+=head2 Create PARAMHASH
+
+Create a new link object. Takes 'Base', 'Target' and 'Type'.
+Returns undef on failure or a Link Id on success.
+
+=cut
+
+sub Create  {
+    my $self = shift;
+    my %args = ( Base => undef,
+                Target => undef,
+                Type => undef,
+                @_ # get the real argumentlist
+              );
+    
+    my $BaseURI = $self->CanonicalizeURI($args{'Base'});
+    my $TargetURI = $self->CanonicalizeURI($args{'Target'});
+    
+    unless (defined $BaseURI) {
+       $RT::Logger->warning ("$self couldn't resolve base:'".$args{'Base'}.
+                             "' into a URI\n");
+       return (undef);
+    }
+    unless (defined $TargetURI) {
+       $RT::Logger->warning ("$self couldn't resolve target:'".$args{'Target'}.
+                             "' into a URI\n");
+       return(undef);
+    }
+    
+    my $LocalBase = $self->_IsLocal($BaseURI);
+    my $LocalTarget = $self->_IsLocal($TargetURI);
+    my $id = $self->SUPER::Create(Base => "$BaseURI",
+                                 Target => "$TargetURI",
+                                 LocalBase => $LocalBase, 
+                                 LocalTarget => $LocalTarget,
+                                 Type => $args{'Type'});
+    return ($id);
+}
+
+# }}}
+# {{{ sub Load 
+
+=head2 Load
+
+  Load an RT::Link object from the database.  Takes one parameter or three.
+  One parameter is the id of an entry in the links table.  Three parameters are a tuple of (base, linktype, target);
+
+
+=cut
+
+sub Load  {
+  my $self = shift;
+  my $identifier = shift;
+  my $linktype = shift if (@_);
+  my $target = shift if (@_);
+  
+  if ($target) {
+      my $BaseURI = $self->CanonicalizeURI($identifier);
+      my $TargetURI = $self->CanonicalizeURI($target);
+      $self->LoadByCols( Base => $BaseURI,
+                        Type => $linktype,
+                        Target => $TargetURI
+                      ) || return (0, "Couldn't load link");
+  }
+  
+  elsif ($identifier =~ /^\d+$/) {
+      $self->LoadById($identifier) ||
+       return (0, "Couldn't load link");
+  }
+  else {
+       return (0, "That's not a numerical id");
+  }
+}
+
+# }}}
+
+# {{{ sub TargetObj 
+
+=head2 TargetObj
+
+=cut
+
+sub TargetObj {
+  my $self = shift;
+   return $self->_TicketObj('base',$self->Target);
+}
+# }}}
+
+# {{{ sub BaseObj
+
+=head2 BaseObj
+
+=cut
+
+sub BaseObj {
+  my $self = shift;
+  return $self->_TicketObj('target',$self->Base);
+}
+# }}}
+
+# {{{ sub _TicketObj
+sub _TicketObj {
+  my $self = shift;
+  my $name = shift;
+  my $ref = shift;
+  my $tag="$name\_obj";
+  
+  unless (exists $self->{$tag}) {
+
+  $self->{$tag}=RT::Ticket->new($self->CurrentUser);
+
+  #If we can get an actual ticket, load it up.
+  if ($self->_IsLocal($ref)) {
+      $self->{$tag}->Load($ref);
+    }
+  }
+  return $self->{$tag};
+}
+# }}}
+
+# {{{ sub _Accessible 
+sub _Accessible  {
+  my $self = shift;
+  my %Cols = (
+             LocalBase => 'read',
+             LocalTarget => 'read',
+             Base => 'read',
+             Target => 'read',
+             Type => 'read',
+             Creator => 'read/auto',
+             Created => 'read/auto',
+             LastUpdatedBy => 'read/auto',
+             LastUpdated => 'read/auto'
+            );
+  return($self->SUPER::_Accessible(@_, %Cols));
+}
+# }}}
+
+
+# Static methods:
+
+# {{{ sub BaseIsLocal
+
+=head2 BaseIsLocal
+
+Returns true if the base of this link is a local ticket
+
+=cut
+
+sub BaseIsLocal {
+  my $self = shift;
+  return $self->_IsLocal($self->Base);
+}
+
+# }}}
+
+# {{{ sub TargetIsLocal
+
+=head2 TargetIsLocal
+
+Returns true if the target of this link is a local ticket
+
+=cut
+
+sub TargetIsLocal {
+  my $self = shift;
+  return $self->_IsLocal($self->Target);
+}
+
+# }}}
+
+# {{{ sub _IsLocal
+
+=head2 _IsLocal URI 
+
+When handed a URI returns the local ticket id if it\'s local. otherwise returns undef.
+
+=cut
+
+sub _IsLocal {
+    my $self = shift;
+    my $URI=shift;
+    unless ($URI) {
+       $RT::Logger->warning ("$self _IsLocal called without a URI\n");
+       return (undef);
+    }
+    # TODO: More thorough check
+    if ($URI =~ /^$RT::TicketBaseURI(\d+)$/) {
+       return($1);
+    }
+    else {
+       return (undef);
+    }
+}
+# }}}
+
+
+# {{{ sub BaseAsHREF 
+
+=head2 BaseAsHREF
+
+Returns an HTTP url to access the base of this link
+
+=cut
+
+sub BaseAsHREF {
+  my $self = shift;
+  return $self->AsHREF($self->Base);
+}
+# }}}
+
+# {{{ sub TargetAsHREF 
+
+=head2 TargetAsHREF
+
+return an HTTP url to access the target of this link
+
+=cut
+
+sub TargetAsHREF {
+  my $self = shift;
+  return $self->AsHREF($self->Target);
+}
+# }}}
+
+# {{{ sub AsHREF - Converts Link URIs to HTTP URLs
+=head2 URI
+
+Takes a URI and returns an http: url to access that object.
+
+=cut
+sub AsHREF {
+    my $self=shift;
+    my $URI=shift;
+    if ($self->_IsLocal($URI)) {
+       my $url=$RT::WebURL . "Ticket/Display.html?id=$URI";
+       return($url);
+    } 
+    else {
+       my ($protocol) = $URI =~ m|(.*?)://|;
+       unless (exists $RT::URI2HTTP{$protocol}) {
+           $RT::Logger->warning("Linking for protocol $protocol not defined in the config file!");
+           return("");
+       }
+       return $RT::URI2HTTP{$protocol}->($URI);
+    }
+}
+
+# }}}
+
+# {{{ sub GetContent - gets the content from a link
+sub GetContent {
+    my ($self, $URI)= @_;
+    if ($self->_IsLocal($URI)) {
+       die "stub";
+    } else {
+       # Find protocol
+       if ($URI =~ m|^(.*?)://|) {
+           if (exists $RT::ContentFromURI{$1}) {
+               return $RT::ContentFromURI{$1}->($URI);
+           } else {
+               warn "No sub exists for fetching the content from a $1 in $URI";
+           }
+       } else {
+           warn "No protocol specified in $URI";
+       }
+    }
+}
+# }}}
+
+# {{{ sub CanonicalizeURI
+
+=head2 CanonicalizeURI
+
+Takes a single argument: some form of ticket identifier. 
+Returns its canonicalized URI.
+
+Bug: ticket aliases can't have :// in them. URIs must have :// in them.
+
+=cut
+
+sub CanonicalizeURI {
+    my $self = shift;
+    my $id = shift;
+    
+    
+    #If it's a local URI, load the ticket object and return its URI
+    if ($id =~ /^$RT::TicketBaseURI/) {
+       my $ticket = new RT::Ticket($self->CurrentUser);
+       $ticket->Load($id);
+       #If we couldn't find a ticket, return undef.
+       return undef unless (defined $ticket->Id);
+       #$RT::Logger->debug("$self -> CanonicalizeURI was passed $id and returned ".$ticket->URI ." (uri)\n");
+       return ($ticket->URI);
+    }
+    #If it's a remote URI, we're going to punt for now
+    elsif ($id =~ '://' ) {
+       return ($id);
+    }
+  
+    #If the base is an integer, load it as a ticket 
+    elsif ( $id =~ /^\d+$/ ) {
+       
+       #$RT::Logger->debug("$self -> CanonicalizeURI was passed $id. It's a ticket id.\n");
+       my $ticket = new RT::Ticket($self->CurrentUser);
+       $ticket->Load($id);
+       #If we couldn't find a ticket, return undef.
+       return undef unless (defined $ticket->Id);
+       #$RT::Logger->debug("$self returned ".$ticket->URI ." (id #)\n");
+       return ($ticket->URI);
+    }
+
+    #It's not a URI. It's not a numerical ticket ID
+    else { 
+     
+       #If we couldn't find a ticket, return undef.
+       return( undef);
+    
+    }
+
+}
+
+# }}}
+
+1;
diff --git a/rt/lib/RT/Links.pm b/rt/lib/RT/Links.pm
new file mode 100644 (file)
index 0000000..a8180ca
--- /dev/null
@@ -0,0 +1,90 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Links.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Links - A collection of Link objects
+
+=head1 SYNOPSIS
+
+  use RT::Links;
+  my $links = new RT::Links($CurrentUser);
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Links);
+
+=end testing
+
+=cut
+
+package RT::Links;
+use RT::EasySearch;
+use RT::Link;
+
+@ISA= qw(RT::EasySearch);
+
+# {{{ sub _Init  
+sub _Init   {
+  my $self = shift;
+  $self->{'table'} = "Links";
+  $self->{'primary_key'} = "id";
+
+
+  return ( $self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub Limit 
+sub Limit  {
+    my $self = shift;
+    my %args = ( ENTRYAGGREGATOR => 'AND',
+                OPERATOR => '=',
+                @_);
+    
+    #if someone's trying to search for tickets, try to resolve the uris for searching.
+    
+    if (  ( $args{'OPERATOR'} eq '=') and
+         ( $args{'FIELD'}  eq 'Base') or ($args{'FIELD'} eq 'Target')
+       ) {
+       my $dummy = $self->NewItem();
+         $uri = $dummy->CanonicalizeURI($args{'VALUE'});
+    }
+
+
+    # If we're limiting by target, order by base
+    # (Order by the thing that's changing)
+
+    if ( ($args{'FIELD'} eq 'Target') or 
+        ($args{'FIELD'} eq 'LocalTarget') ) {
+       $self->OrderBy (ALIAS => 'main',
+                       FIELD => 'Base',
+                       ORDER => 'ASC');
+    }
+    elsif ( ($args{'FIELD'} eq 'Base') or 
+           ($args{'FIELD'} eq 'LocalBase') ) {
+       $self->OrderBy (ALIAS => 'main',
+                       FIELD => 'Target',
+                       ORDER => 'ASC');
+    }
+    
+
+    $self->SUPER::Limit(%args);
+}
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem  {
+    my $self = shift;
+    return(RT::Link->new($self->CurrentUser));
+}
+# }}}
+  1;
+
diff --git a/rt/lib/RT/ObjectKeyword.pm b/rt/lib/RT/ObjectKeyword.pm
new file mode 100644 (file)
index 0000000..287d41f
--- /dev/null
@@ -0,0 +1,192 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attic/ObjectKeyword.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Released under the terms of the GNU Public License
+
+=head1 NAME
+
+  RT::ObjectKeyword -- a keyword tied to an object in the database
+
+=head1 SYNOPSIS
+
+  use RT::ObjectKeyword;
+
+
+=head1 DESCRIPTION
+
+This module should never be called directly by client code. it's an internal module which
+should only be accessed through exported APIs in Ticket, Queue and other similar objects.
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::ObjectKeyword);
+
+=end testing
+
+=head1 METHODS
+
+=cut
+
+package RT::ObjectKeyword;
+
+use strict;
+use vars qw(@ISA);
+use RT::Record;
+
+@ISA = qw(RT::Record);
+
+sub _Init {
+    my $self = shift;
+    $self->{'table'} = "ObjectKeywords";
+    $self->SUPER::_Init(@_);
+}
+
+sub _Accessible {
+    my $self = shift;
+    
+    my %cols = (
+               Keyword       => 'read/write', #link to the B<RT::Keyword>
+               KeywordSelect => 'read/write', #link to the B<RT::KeywordSelect>
+               ObjectType    => 'read/write', #currently only C<Ticket>
+               ObjectId      => 'read/write', #link to the object specified in I<ObjectType>
+              );
+    return ($self->SUPER::_Accessible( @_, %cols));
+}
+
+
+
+# TODO - post 2.0. add in _Set and _Value, so we can ACL them.  protected at another API level
+
+
+=head1 NAME
+
+ RT::ObjectKeyword - Manipulate an RT::ObjectKeyword record
+
+=head1 SYNOPSIS
+
+  use RT::ObjectKeyword;
+
+  my $keyword = RT::ObjectKeyword->new($CurrentUser);
+  $keyword->Create;
+
+=head1 DESCRIPTION
+
+An B<RT::ObjectKeyword> object associates an B<RT::Keyword> with another
+object (currently only B<RT::Ticket>.
+
+This module should B<NEVER> be called directly by client code. its API is entirely through RT ticket or other objects which can have keywords assigned.
+
+
+=head1 METHODS
+
+=over 4
+
+=item new CURRENT_USER
+
+Takes a single argument, an RT::CurrentUser object.  Instantiates a new
+(uncreated) RT::ObjectKeyword object.
+
+=cut
+
+# {{{ sub Create
+
+=item Create KEY => VALUE, ...
+
+Takes a list of key/value pairs and creates a the object.  Returns the id of
+the newly created record, or false if there was an error.
+
+Keys are:
+
+Keyword - link to the B<RT::Keyword>
+ObjectType - currently only C<Ticket>
+ObjectId - link to the object specified in I<ObjectType>
+
+=cut
+
+
+sub Create {
+    my $self = shift;
+    my %args = (Keyword => undef,
+               KeywordSelect => undef,
+               ObjectType => undef,
+               ObjectId => undef,
+               @_);
+    
+    #TODO post 2.0 ACL check
+    
+    return ($self->SUPER::Create( Keyword => $args{'Keyword'}, 
+                                 KeywordSelect => $args{'KeywordSelect'},
+                                 ObjectType => $args{'ObjectType'}, 
+                                 ObjectId => $args{'ObjectId'}))
+}
+# }}}
+
+# {{{ sub KeywordObj
+
+=item KeywordObj 
+
+Returns an B<RT::Keyword> object of the Keyword associated with this ObjectKeyword.
+
+=cut
+
+sub KeywordObj {
+    my $self = shift;
+    my $keyword = new RT::Keyword($self->CurrentUser);
+    $keyword->Load($self->Keyword);
+    return ($keyword);
+}
+# }}}
+
+# {{{ sub KeywordSelectObj
+
+=item KeywordSelectObj 
+
+Returns an B<RT::KeywordSelect> object of the KeywordSelect associated with this ObjectKeyword.
+
+=cut
+
+sub KeywordSelectObj {
+    my $self = shift;
+    my $keyword_sel = new RT::KeywordSelect($self->CurrentUser);
+    $keyword_sel->Load($self->KeywordSelect);
+    return ($keyword_sel);
+}
+# }}}
+
+# {{{ sub KeywordRelativePath
+
+=item KeywordRelativePath
+
+Returns a string of the Keyword's path relative to this ObjectKeyword's KeywordSelect
+
+
+
+=cut
+
+sub KeywordRelativePath {
+    my $self = shift;
+    return($self->KeywordObj->RelativePath(
+              $self->KeywordSelectObj->KeywordObj->Path));
+    
+}
+# }}}
+
+=back
+
+=head1 AUTHOR
+
+Ivan Kohler <ivan-rt@420.am>
+
+=head1 BUGS
+
+Yes.
+
+=head1 SEE ALSO
+
+L<RT::ObjectKeywords>, L<RT::Keyword>, L<RT::Keywords>, L<RT::Ticket>,
+L<RT::Record>
+
+=cut
+
+1;
+
diff --git a/rt/lib/RT/ObjectKeywords.pm b/rt/lib/RT/ObjectKeywords.pm
new file mode 100644 (file)
index 0000000..5df996e
--- /dev/null
@@ -0,0 +1,234 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attic/ObjectKeywords.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+package RT::ObjectKeywords;
+
+use strict;
+use vars qw( @ISA );
+
+=head1 NAME
+
+       RT::ObjectKeywords - note warning
+
+=head1 WARNING
+
+This module should B<NEVER> be called directly by client code. its API is entirely through RT ticket or other objects which can have keywords assigned.
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::ObjectKeywords);
+
+=end testing
+
+=cut
+
+use RT::EasySearch;
+use RT::ObjectKeyword;
+
+@ISA = qw( RT::EasySearch );
+
+# {{{ sub _Init
+sub _Init {
+    my $self = shift;
+    $self->{'table'} = 'ObjectKeywords';
+    $self->{'primary_key'} = 'id';
+    return ($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub NewItem
+sub NewItem {
+    my $self = shift;
+    return (new RT::ObjectKeyword($self->CurrentUser));
+}
+# }}}
+
+# {{{ sub LimitToKeywordSelect
+
+=head2 LimitToKeywordSelect
+
+  Takes a B<RT::KeywordSelect> id or Nameas its single argument. limits the returned set of ObjectKeywords
+to ObjectKeywords which apply to that ticket
+
+=cut
+
+
+sub LimitToKeywordSelect {
+    my $self = shift;
+    my $keywordselect = shift;
+    
+    if ($keywordselect =~ /^\d+$/) {
+
+       $self->Limit(FIELD => 'KeywordSelect',
+                    OPERATOR => '=',
+                    ENTRYAGGREGATOR => 'OR',
+                    VALUE => "$keywordselect");
+    }
+
+    #We're limiting by name. time to be klever
+    else {
+       my $ks = $self->NewAlias('KeywordSelects');
+       $self->Join(ALIAS1 => $ks, FIELD1 => 'id',
+                   ALIAS2 => 'main', FIELD2 => 'KeywordSelect');
+       
+       $self->Limit( ALIAS => "$ks",
+                      FIELD => 'Name',
+                      VALUE => "$keywordselect",
+                      OPERATOR => "=",
+                      ENTRYAGGREGATOR => "OR");
+
+       $self->Limit ( ALIAS => "$ks",
+                      FIELD => 'ObjectType',
+                      VALUE => 'Ticket',
+                      OPERATOR => '=',
+                    );
+
+       $self->Limit ( ALIAS => "$ks",
+                      FIELD => 'ObjectField',
+                      VALUE => 'Queue',
+                      OPERATOR => '=',
+                    );
+
+
+       # TODO +++ we need to be able to limit the returned
+       # keywordselects to ones that apply only to this queue
+       #       $self->Limit( ALIAS => "$ks",
+       #                      FIELD => 'ObjectValue',
+       #                      VALUE => $self->QueueObj->Id,
+       #                      OPERATOR => "=",
+       #                      ENTRYAGGREGATOR => "OR");        
+
+    }
+
+
+
+}
+
+# }}}
+
+# {{{ LimitToTicket
+
+=head2 LimitToTicket TICKET_ID
+
+  Takes an B<RT::Ticket> id as its single argument. limits the returned set of ObjectKeywords
+to ObjectKeywords which apply to that ticket
+
+=cut
+
+sub LimitToTicket {
+    my $self = shift;
+    my $ticket = shift;
+    $self->Limit(FIELD => 'ObjectId',
+                OPERATOR => '=',
+                ENTRYAGGREGATOR => 'OR',
+                VALUE => "$ticket");
+
+    $self->Limit(FIELD => 'ObjectType',
+                OPERATOR => '=',
+                ENTRYAGGREGATOR => 'OR',
+                VALUE => "Ticket");
+    
+}
+
+# }}}
+
+# {{{ sub _DoSearch
+#wrap around _DoSearch  so that we can build the hash of returned
+#values 
+
+sub _DoSearch {
+    my $self = shift;
+   # $RT::Logger->debug("Now in ".$self."->_DoSearch");
+    my $return = $self->SUPER::_DoSearch(@_);
+  #  $RT::Logger->debug("In $self ->_DoSearch. return from SUPER::_DoSearch was $return\n");
+    $self->_BuildHash();
+    return ($return);
+}
+# }}}
+
+# {{{ sub _BuildHash
+#Build a hash of this ACL's entries.
+sub _BuildHash {
+    my $self = shift;
+
+    while (my $entry = $self->Next) {
+
+       my $hashkey = $entry->Keyword;
+        $self->{'as_hash'}->{"$hashkey"} =1;
+    }
+
+}
+# }}}
+
+# {{{ HasEntry
+
+=head2 HasEntry KEYWORD_ID
+  
+  Takes a keyword id and returns true if this ObjectKeywords object has an entry for that
+keyword.  Returns undef otherwise.
+
+=cut
+
+sub HasEntry {
+
+    my $self = shift;
+    my $keyword = shift;
+
+
+    #if we haven't done the search yet, do it now.
+    $self->_DoSearch();
+    
+    #    $RT::Logger->debug("Now in ".$self."->HasEntry\n");
+    
+    
+    if ($self->{'as_hash'}->{ $keyword } == 1) {
+       return(1);
+    }
+    else {
+       return(undef);
+    }
+}
+
+# }}}
+
+# {{{ sub RelativePaths
+
+=head2 RelativePaths
+
+# Return a (reference to a) list of KeywordRelativePaths
+
+=cut
+
+sub RelativePaths  {
+    my $self = shift;
+
+    my @list;
+
+    # Here $key is a RT::ObjectKeyword
+    while (my $key=$self->Next()) {
+       push(@list, $key->KeywordRelativePath);
+    }
+    return(\@list);
+}
+# }}}
+
+# {{{ sub RelativePathsAsString
+
+=head2 RelativePathsAsString
+
+# Returns the RT::ObjectKeywords->RelativePaths as a comma seperated string
+
+=cut
+
+sub RelativePathsAsString {
+    my $self = shift;
+    return(join(", ",@{$self->KeywordRelativePaths}));
+}
+# }}}
+
+1;
+
diff --git a/rt/lib/RT/Queue.pm b/rt/lib/RT/Queue.pm
new file mode 100755 (executable)
index 0000000..1656903
--- /dev/null
@@ -0,0 +1,944 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Queue.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Queue - an RT Queue object
+
+=head1 SYNOPSIS
+
+  use RT::Queue;
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing 
+use RT::TestHarness;
+
+use RT::Queue;
+
+=end testing
+
+=cut
+
+
+
+package RT::Queue;
+use RT::Record;
+
+@ISA= qw(RT::Record);
+
+use vars (@STATUS);
+
+@STATUS = qw(new open stalled resolved dead); 
+
+=head2 StatusArray
+
+Returns an array of all statuses for this queue
+
+=cut
+
+sub StatusArray {
+    my $self = shift;
+    return (@STATUS);
+}
+
+
+=head2 IsValidStatus VALUE
+
+Returns true if VALUE is a valid status.  Otherwise, returns 0
+
+=for testing
+my $q = new RT::Queue($RT::SystemUser);
+ok($q->IsValidStatus('new')== 1, 'New is a valid status');
+ok($q->IsValidStatus('f00')== 0, 'f00 is not a valid status');
+
+=cut
+
+sub IsValidStatus {
+       my $self = shift;
+       my $value = shift;
+
+       my $retval = grep (/^$value$/, $self->StatusArray);
+       return ($retval);       
+
+}
+       
+
+
+
+# {{{  sub _Init 
+sub _Init  {
+    my $self = shift;
+    $self->{'table'} = "Queues";
+    return ($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _Accessible 
+
+sub _Accessible  {
+    my $self = shift;
+    my %Cols = ( Name => 'read/write',
+                CorrespondAddress => 'read/write',
+                Description => 'read/write',
+                CommentAddress =>  'read/write',
+                InitialPriority =>  'read/write',
+                FinalPriority =>  'read/write',
+                DefaultDueIn =>  'read/write',
+                Creator => 'read/auto',
+                Created => 'read/auto',
+                LastUpdatedBy => 'read/auto',
+                LastUpdated => 'read/auto',
+                Disabled => 'read/write',
+                
+              );
+    return($self->SUPER::_Accessible(@_, %Cols));
+}
+
+# }}}
+
+# {{{ sub Create
+
+=head2 Create
+
+Create takes the name of the new queue 
+If you pass the ACL check, it creates the queue and returns its queue id.
+
+=cut
+
+sub Create  {
+    my $self = shift;
+    my %args = ( Name => undef,
+                CorrespondAddress => '',
+                Description => '',
+                CommentAddress => '',
+                InitialPriority => "0",
+                FinalPriority =>  "0",
+                DefaultDueIn =>  "0",
+                @_); 
+    
+    unless ($self->CurrentUser->HasSystemRight('AdminQueue')) {    #Check them ACLs
+       return (0, "No permission to create queues") 
+    }
+
+    unless ($self->ValidateName($args{'Name'})) {
+       return(0, 'Queue already exists');
+    }
+    #TODO better input validation
+    
+    my $id = $self->SUPER::Create(%args);
+    unless ($id) {
+       return (0, 'Queue could not be created');
+    }
+
+    return ($id, "Queue $id created");
+}
+
+# }}}
+
+# {{{ sub Delete 
+
+sub Delete {
+    my $self = shift;
+    return (0, 'Deleting this object would break referential integrity');
+}
+
+# }}}
+
+# {{{ sub SetDisabled
+
+=head2 SetDisabled
+
+Takes a boolean.
+1 will cause this queue to no longer be avaialble for tickets.
+0 will re-enable this queue
+
+=cut
+
+# }}}
+
+# {{{ sub Load 
+
+=head2 Load
+
+Takes either a numerical id or a textual Name and loads the specified queue.
+  
+=cut
+
+sub Load  {
+    my $self = shift;
+    
+    my $identifier = shift;
+    if (!$identifier) {
+       return (undef);
+    }      
+    
+    if ($identifier !~ /\D/) {
+       $self->SUPER::LoadById($identifier);
+    }
+    else {
+       $self->LoadByCol("Name", $identifier);
+    }
+
+    return ($self->Id);
+
+
+}
+# }}}
+
+# {{{ sub ValidateName
+
+=head2 ValidateName NAME
+
+Takes a queue name. Returns true if it's an ok name for
+a new queue. Returns undef if there's already a queue by that name.
+
+=cut
+
+sub ValidateName {
+    my $self = shift;
+    my $name = shift;
+   
+    my $tempqueue = new RT::Queue($RT::SystemUser);
+    $tempqueue->Load($name);
+
+    #If we couldn't load it :)
+    unless ($tempqueue->id()) {
+       return(1);
+    }
+
+    #If this queue exists, return undef
+    #Avoid the ACL check.
+    if ($tempqueue->Name()){
+        return(undef);
+    }
+
+    #If the queue doesn't exist, return 1
+    else {
+        return(1);
+    }
+
+}
+
+
+# }}}
+
+# {{{ sub Templates
+
+=head2 Templates
+
+Returns an RT::Templates object of all of this queue's templates.
+
+=cut
+
+sub Templates {
+    my $self = shift;
+    
+
+    my $templates = RT::Templates->new($self->CurrentUser);
+
+    if ($self->CurrentUserHasRight('ShowTemplate')) {
+       $templates->LimitToQueue($self->id);
+    }
+    
+    return ($templates); 
+}
+
+# }}}
+
+# {{{ Dealing with watchers
+
+# {{{ sub Watchers
+
+=head2 Watchers
+
+Watchers returns a Watchers object preloaded with this queue\'s watchers.
+
+=cut
+
+sub Watchers {
+    my $self = shift;
+    
+    require RT::Watchers;
+    my $watchers =RT::Watchers->new($self->CurrentUser);
+    
+    if ($self->CurrentUserHasRight('SeeQueue')) {
+       $watchers->LimitToQueue($self->id);     
+    }  
+    
+    return($watchers);
+}
+
+# }}}
+
+# {{{ sub WatchersAsString
+=head2 WatchersAsString
+
+Returns a string of all queue watchers email addresses concatenated with ','s.
+
+=cut
+
+sub WatchersAsString {
+    my $self=shift;
+    return($self->Watchers->EmailsAsString());
+}
+
+# }}}
+
+# {{{ sub AdminCcAsString 
+
+=head2 AdminCcAsString
+
+Takes nothing. returns a string: All Ticket/Queue AdminCcs.
+
+=cut
+
+
+sub AdminCcAsString {
+    my $self=shift;
+    
+    return($self->AdminCc->EmailsAsString());
+  }
+
+# }}}
+
+# {{{ sub CcAsString
+
+=head2 CcAsString
+
+B<Returns> String: All Queue Ccs as a comma delimited set of email addresses.
+
+=cut
+
+sub CcAsString {
+    my $self=shift;
+    
+    return ($self->Cc->EmailsAsString());
+}
+
+# }}}
+
+# {{{ sub Cc
+
+=head2 Cc
+
+Takes nothing.
+Returns a watchers object which contains this queue\'s Cc watchers
+
+=cut
+
+sub Cc {
+    my $self = shift;
+    my $cc = $self->Watchers();
+    if ($self->CurrentUserHasRight('SeeQueue')) {
+       $cc->LimitToCc();
+    }
+    return ($cc);
+}
+
+# A helper function for Cc, so that we can call it from the ACL checks 
+# without going through acl checks.
+
+sub _Cc {
+    my $self = shift;
+    my $cc = $self->Watchers();
+    $cc->LimitToCc();
+    return($cc);
+    
+}
+
+# }}}
+
+# {{{ sub AdminCc
+
+=head2 AdminCc
+
+Takes nothing.
+Returns this queue's administrative Ccs as an RT::Watchers object
+
+=cut
+
+sub AdminCc {
+    my $self = shift;
+    my $admin_cc = $self->Watchers();
+    if ($self->CurrentUserHasRight('SeeQueue')) {
+       $admin_cc->LimitToAdminCc();
+    }
+    return($admin_cc);
+}
+
+#helper function for AdminCc so we can call it without ACLs
+sub _AdminCc {
+    my $self = shift;
+    my $admin_cc = $self->Watchers();
+    $admin_cc->LimitToAdminCc();
+    return($admin_cc);
+}
+
+# }}}
+
+# {{{ IsWatcher, IsCc, IsAdminCc
+
+# {{{ sub IsWatcher
+
+# a generic routine to be called by IsRequestor, IsCc and IsAdminCc
+
+=head2 IsWatcher
+
+Takes a param hash with the attributes Type and User. User is either a user object or string containing an email address. Returns true if that user or string
+is a queue watcher. Returns undef otherwise
+
+=cut
+
+sub IsWatcher {
+    my $self = shift;
+    
+    my %args = ( Type => 'Requestor',
+                Id => undef,
+                Email => undef,
+                @_
+              );
+    #ACL check - can't do it. we need this method for ACL checks
+    #    unless ($self->CurrentUserHasRight('SeeQueue')) {
+    #  return(undef);
+    #    }
+
+
+    my %cols = ('Type' => $args{'Type'},
+               'Scope' => 'Queue',
+               'Value' => $self->Id
+              );
+    if (defined ($args{'Id'})) {
+       if (ref($args{'Id'})){ #If it's a ref, assume it's an RT::User object;
+           #Dangerous but ok for now
+           $cols{'Owner'} = $args{'Id'}->Id;
+       }
+       elsif ($args{'Id'} =~ /^\d+$/) { # if it's an integer, it's an RT::User obj
+           $cols{'Owner'} = $args{'Id'};
+       }
+       else {
+           $cols{'Email'} = $args{'Id'};
+       }       
+    }  
+    
+    if (defined $args{'Email'}) {
+       $cols{'Email'} = $args{'Email'};
+    }
+
+    my ($description);
+    $description = join(":",%cols);
+    
+    #If we've cached a positive match...
+    if (defined $self->{'watchers_cache'}->{"$description"}) {
+       if ($self->{'watchers_cache'}->{"$description"} == 1) {
+           return(1);
+       }
+       #If we've cached a negative match...
+       else {
+           return(undef);
+       }
+    }
+
+    require RT::Watcher;
+    my $watcher = new RT::Watcher($self->CurrentUser);
+    $watcher->LoadByCols(%cols);
+    
+    
+    if ($watcher->id) {
+       $self->{'watchers_cache'}->{"$description"} = 1;
+       return(1);
+    }  
+    else {
+       $self->{'watchers_cache'}->{"$description"} = 0;
+       return(undef);
+    }
+    
+}
+
+# }}}
+
+# {{{ sub IsCc
+
+=head2 IsCc
+
+Takes a string. Returns true if the string is a Cc watcher of the current queue
+
+=item Bugs
+
+Should also be able to handle an RT::User object
+
+=cut
+
+
+sub IsCc {
+  my $self = shift;
+  my $cc = shift;
+  
+  return ($self->IsWatcher( Type => 'Cc', Id => $cc ));
+  
+}
+
+# }}}
+
+# {{{ sub IsAdminCc
+
+=head2 IsAdminCc
+
+Takes a string. Returns true if the string is an AdminCc watcher of the current queue
+
+=item Bugs
+
+Should also be able to handle an RT::User object
+
+=cut
+
+sub IsAdminCc {
+  my $self = shift;
+  my $admincc = shift;
+  
+  return ($self->IsWatcher( Type => 'AdminCc', Id => $admincc ));
+  
+}
+
+# }}}
+
+# }}}
+
+# {{{ sub AddWatcher
+
+=head2 AddWatcher
+
+Takes a paramhash of Email, Owner and Type. Type is one of 'Cc' or 'AdminCc',
+We need either an Email Address in Email or a userid in Owner
+
+=cut
+
+sub AddWatcher {
+    my $self = shift;
+    my %args = ( Email => undef,
+                Type => undef,
+                Owner => 0,
+                @_
+              );
+    
+    # {{{ Check ACLS
+    #If the watcher we're trying to add is for the current user
+    if ( ( ( defined $args{'Email'})  && 
+           ( $args{'Email'} eq $self->CurrentUser->EmailAddress) ) or 
+        ($args{'Owner'} eq $self->CurrentUser->Id)) {
+       
+       #  If it's an AdminCc and they don't have 
+       #   'WatchAsAdminCc' or 'ModifyQueueWatchers', bail
+       if ($args{'Type'} eq 'AdminCc') {
+           unless ($self->CurrentUserHasRight('ModifyQueueWatchers') or 
+                   $self->CurrentUserHasRight('WatchAsAdminCc')) {
+               return(0, 'Permission Denied');
+           }
+       }
+
+       #  If it's a Requestor or Cc and they don't have
+       #   'Watch' or 'ModifyQueueWatchers', bail
+       elsif ($args{'Type'} eq 'Cc') {
+           unless ($self->CurrentUserHasRight('ModifyQueueWatchers') or 
+                   $self->CurrentUserHasRight('Watch')) {
+               return(0, 'Permission Denied');
+           }
+       }
+       else {
+           $RT::Logger->warn("$self -> AddWatcher hit code".
+                             " it never should. We got passed ".
+                             " a type of ". $args{'Type'});
+           return (0,'Error in parameters to $self AddWatcher');
+       }
+    }
+    # If the watcher isn't the current user 
+    # and the current user  doesn't have 'ModifyQueueWatchers'
+    # bail
+    else {
+       unless ($self->CurrentUserHasRight('ModifyQueueWatchers')) {
+           return (0, "Permission Denied");
+       }
+    }
+    # }}}
+        
+    require RT::Watcher;
+    my $Watcher = new RT::Watcher ($self->CurrentUser);
+    return ($Watcher->Create(Scope => 'Queue', 
+                            Value => $self->Id,
+                            Email => $args{'Email'},
+                            Type => $args{'Type'},
+                            Owner => $args{'Owner'}
+                           ));
+}
+
+# }}}
+
+# {{{ sub AddCc
+
+=head2 AddCc
+
+Add a Cc to this queue.
+Takes a paramhash of Email and Owner. 
+We need either an Email Address in Email or a userid in Owner
+
+=cut
+
+
+sub AddCc {
+    my $self = shift;
+    return ($self->AddWatcher( Type => 'Cc', @_));
+}
+# }}}
+
+# {{{ sub AddAdminCc
+
+=head2 AddAdminCc
+
+Add an Administrative Cc to this queue.
+Takes a paramhash of Email and Owner. 
+We need either an Email Address in Email or a userid in Owner
+
+=cut
+
+sub AddAdminCc {
+    my $self = shift;
+    return ($self->AddWatcher( Type => 'AdminCc', @_));
+}
+# }}}
+
+# {{{ sub DeleteWatcher
+
+=head2 DeleteWatcher id [type]
+
+DeleteWatcher takes a single argument which is either an email address 
+or a watcher id.  
+If the first argument is an email address, you need to specify the watcher type you're talking
+about as the second argument. Valid values are 'Cc' or 'AdminCc'.
+It removes that watcher from this Queue\'s list of watchers.
+
+
+=cut
+
+
+sub DeleteWatcher {
+    my $self = shift;
+    my $id = shift;
+    
+    my $type;
+    
+    $type = shift if (@_);
+    
+
+    require RT::Watcher;
+    my $Watcher = new RT::Watcher($self->CurrentUser);
+    
+    #If it\'s a numeric watcherid
+    if ($id =~ /^(\d*)$/) {
+       $Watcher->Load($id);
+    }
+    
+    #Otherwise, we'll assume it's an email address
+    elsif ($type) {
+       my ($result, $msg) = 
+         $Watcher->LoadByValue( Email => $id,
+                                Scope => 'Queue',
+                                Value => $self->id,
+                                Type => $type);
+       return (0,$msg) unless ($result);
+    }
+    
+    else {
+       return(0,"Can\'t delete a watcher by email address without specifying a type");
+    }
+    
+    # {{{ Check ACLS 
+
+    #If the watcher we're trying to delete is for the current user
+    if ($Watcher->Email eq $self->CurrentUser->EmailAddress) {
+               
+       #  If it's an AdminCc and they don't have 
+       #   'WatchAsAdminCc' or 'ModifyQueueWatchers', bail
+       if ($Watcher->Type eq 'AdminCc') {
+           unless ($self->CurrentUserHasRight('ModifyQueueWatchers') or 
+                   $self->CurrentUserHasRight('WatchAsAdminCc')) {
+               return(0, 'Permission Denied');
+           }
+       }
+
+       #  If it's a  Cc and they don't have
+       #   'Watch' or 'ModifyQueueWatchers', bail
+       elsif ($Watcher->Type eq 'Cc') {
+           unless ($self->CurrentUserHasRight('ModifyQueueWatchers') or 
+                   $self->CurrentUserHasRight('Watch')) {
+               return(0, 'Permission Denied');
+           }
+       }
+       else {
+           $RT::Logger->warn("$self -> DeleteWatcher hit code".
+                             " it never should. We got passed ".
+                             " a type of ". $args{'Type'});
+           return (0,'Error in parameters to $self DeleteWatcher');
+       }
+    }
+    # If the watcher isn't the current user 
+    # and the current user  doesn't have 'ModifyQueueWatchers'
+    # bail
+    else {
+       unless ($self->CurrentUserHasRight('ModifyQueueWatchers')) {
+           return (0, "Permission Denied");
+       }
+    }
+
+    # }}}
+    
+    unless (($Watcher->Scope eq 'Queue') and
+           ($Watcher->Value == $self->id) ) {
+       return (0, "Not a watcher for this queue");
+    }
+    
+
+    #Clear out the watchers hash.
+    $self->{'watchers'} = undef;
+    
+    my $retval = $Watcher->Delete();
+    
+    unless ($retval) {
+       return(0,"Watcher could not be deleted.");
+    }
+    
+    return(1, "Watcher deleted");
+}
+
+# {{{ sub DeleteCc
+
+=head2 DeleteCc EMAIL
+
+Takes an email address. It calls DeleteWatcher with a preset 
+type of 'Cc'
+
+
+=cut
+
+sub DeleteCc {
+   my $self = shift;
+   my $id = shift;
+   return ($self->DeleteWatcher ($id, 'Cc'))
+}
+
+# }}}
+
+# {{{ sub DeleteAdminCc
+
+=head2 DeleteAdminCc EMAIL
+
+Takes an email address. It calls DeleteWatcher with a preset 
+type of 'AdminCc'
+
+
+=cut
+
+sub DeleteAdminCc {
+   my $self = shift;
+   my $id = shift;
+   return ($self->DeleteWatcher ($id, 'AdminCc'))
+}
+
+# }}}
+
+
+# }}}
+
+# }}}
+
+# {{{ Dealing with keyword selects
+
+# {{{ sub AddKeywordSelect
+
+=head2 AddKeywordSelect
+
+Takes a paramhash of Name, Keyword, Depth and Single.  Adds a new KeywordSelect for 
+this queue with those attributes.
+
+=cut
+
+
+sub AddKeywordSelect {
+    my $self = shift;
+    my %args = ( Keyword => undef,
+                Depth => undef,
+                Single => undef,
+                Name => undef,
+                @_);
+    
+    #ACLS get handled in KeywordSelect
+    my $NewKeywordSelect = new RT::KeywordSelect($self->CurrentUser);
+    
+    return ($NewKeywordSelect->Create (Keyword => $args{'Keyword'},
+                              Depth => $args{'Depth'},
+                              Name => $args{'Name'},
+                              Single => $args{'Single'},
+                              ObjectType => 'Ticket',
+                              ObjectField => 'Queue',
+                              ObjectValue => $self->Id()
+                             ) );
+}
+
+# }}}
+
+# {{{ sub KeywordSelect
+
+=head2 KeywordSelect([NAME])
+
+Takes the name of a keyword select for this queue or that's global.
+Returns the relevant KeywordSelect object.  Prefers a keywordselect that's 
+specific to this queue over a global one.  If it can't find the proper
+Keword select or the user doesn't have permission, returns an empty 
+KeywordSelect object
+
+=cut
+
+sub KeywordSelect {
+    my $self = shift;
+    my $name = shift;
+    
+    require RT::KeywordSelect;
+
+    my $select = RT::KeywordSelect->new($self->CurrentUser);
+    if ($self->CurrentUserHasRight('SeeQueue')) {
+       $select->LoadByName( Name => $name, Queue => $self->Id);
+    }
+    return ($select);
+}
+
+
+# }}}
+
+# {{{ sub KeywordSelects
+
+=head2 KeywordSelects
+
+Returns an B<RT::KeywordSelects> object containing the collection of
+B<RT::KeywordSelect> objects which apply to this queue. (Both queue specific keyword selects
+and global keyword selects.
+
+=cut
+
+sub KeywordSelects {
+  my $self = shift;
+
+
+  use RT::KeywordSelects;
+  my $KeywordSelects = new RT::KeywordSelects($self->CurrentUser);
+
+  if ($self->CurrentUserHasRight('SeeQueue')) {
+      $KeywordSelects->LimitToQueue($self->id);
+      $KeywordSelects->IncludeGlobals();
+  }
+  return ($KeywordSelects);
+}
+# }}}
+
+# }}}
+
+# {{{ ACCESS CONTROL
+
+# {{{ sub ACL 
+
+=head2 ACL
+
+#Returns an RT::ACL object of ACEs everyone who has anything to do with this queue.
+
+=cut
+
+sub ACL  {
+    my $self = shift;
+    
+    use RT::ACL;
+    my $acl = new RT::ACL($self->CurrentUser);
+    
+    if ($self->CurrentUserHasRight('ShowACL')) {
+       $acl->LimitToQueue($self->Id);
+    }
+    
+    return ($acl);
+}
+
+# }}}
+
+# {{{ sub _Set
+sub _Set {
+    my $self = shift;
+
+    unless ($self->CurrentUserHasRight('AdminQueue')) {
+       return(0, 'Permission Denied');
+    }  
+    return ($self->SUPER::_Set(@_));
+}
+# }}}
+
+# {{{ sub _Value
+
+sub _Value {
+    my $self = shift;
+
+    unless ($self->CurrentUserHasRight('SeeQueue')) {
+       return (undef);
+    }
+
+    return ($self->__Value(@_));
+}
+
+# }}}
+
+# {{{ sub CurrentUserHasRight
+
+=head2 CurrentUserHasRight
+
+Takes one argument. A textual string with the name of the right we want to check.
+Returns true if the current user has that right for this queue.
+Returns undef otherwise.
+
+=cut
+
+sub CurrentUserHasRight {
+  my $self = shift;
+  my $right = shift;
+
+  return ($self->HasRight( Principal=> $self->CurrentUser,
+                            Right => "$right"));
+
+}
+
+# }}}
+
+# {{{ sub HasRight
+
+=head2 HasRight
+
+Takes a param hash with the fields 'Right' and 'Principal'.
+Principal defaults to the current user.
+Returns true if the principal has that right for this queue.
+Returns undef otherwise.
+
+=cut
+
+# TAKES: Right and optional "Principal" which defaults to the current user
+sub HasRight {
+    my $self = shift;
+        my %args = ( Right => undef,
+                     Principal => $self->CurrentUser,
+                     @_);
+        unless(defined $args{'Principal'}) {
+                $RT::Logger->debug("Principal undefined in Queue::HasRight");
+
+        }
+        return($args{'Principal'}->HasQueueRight(QueueObj => $self,
+          Right => $args{'Right'}));
+}
+# }}}
+
+# }}}
+
+1;
diff --git a/rt/lib/RT/Queues.pm b/rt/lib/RT/Queues.pm
new file mode 100755 (executable)
index 0000000..ab58d8d
--- /dev/null
@@ -0,0 +1,123 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Queues.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Queues - a collection of RT::Queue objects
+
+=head1 SYNOPSIS
+
+  use RT::Queues;
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Queues);
+
+=end testing
+
+=cut
+
+package RT::Queues;
+use RT::EasySearch;
+@ISA= qw(RT::EasySearch);
+
+
+# {{{ sub _Init
+sub _Init { 
+  my $self = shift;
+  $self->{'table'} = "Queues";
+  $self->{'primary_key'} = "id";
+
+  # By default, order by name
+  $self->OrderBy( ALIAS => 'main',
+                 FIELD => 'Name',
+                 ORDER => 'ASC');
+
+  return ($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _DoSearch 
+
+=head2 _DoSearch
+
+  A subclass of DBIx::SearchBuilder::_DoSearch that makes sure that _Disabled rows never get seen unless
+we're explicitly trying to see them.
+
+=cut
+
+sub _DoSearch {
+    my $self = shift;
+    
+    #unless we really want to find disabled rows, make sure we\'re only finding enabled ones.
+    unless($self->{'find_disabled_rows'}) {
+       $self->LimitToEnabled();
+    }
+    
+    return($self->SUPER::_DoSearch(@_));
+    
+}
+
+# }}}
+  
+
+# {{{ sub Limit 
+sub Limit  {
+  my $self = shift;
+  my %args = ( ENTRYAGGREGATOR => 'AND',
+              @_);
+  $self->SUPER::Limit(%args);
+}
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem  {
+  my $self = shift;
+  my $item;
+
+  use RT::Queue;
+  $item = new RT::Queue($self->CurrentUser);
+  return($item);
+}
+# }}}
+
+# {{{ sub Next 
+
+=head2 Next
+
+Returns the next queue that this user can see.
+
+=cut
+  
+sub Next {
+    my $self = shift;
+    
+    
+    my $Queue = $self->SUPER::Next();
+    if ((defined($Queue)) and (ref($Queue))) {
+
+       if ($Queue->CurrentUserHasRight('SeeQueue')) {
+           return($Queue);
+       }
+       
+       #If the user doesn't have the right to show this queue
+       else {  
+           return($self->Next());
+       }
+    }
+    #if there never was any queue
+    else {
+       return(undef);
+    }  
+    
+}
+# }}}
+
+1;
+
diff --git a/rt/lib/RT/Record.pm b/rt/lib/RT/Record.pm
new file mode 100755 (executable)
index 0000000..5340f7d
--- /dev/null
@@ -0,0 +1,345 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Record.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Record - Base class for RT record objects
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+
+=begin testing
+
+ok (require RT::Record);
+
+=end testing
+
+=head1 METHODS
+
+=cut
+
+
+package RT::Record;
+use DBIx::SearchBuilder::Record::Cachable;
+use RT::Date;
+use RT::User;
+
+@ISA= qw(DBIx::SearchBuilder::Record::Cachable);
+
+# {{{ sub _Init 
+
+sub _Init  {
+  my $self = shift;
+  $self->_MyCurrentUser(@_);
+  
+}
+
+# }}}
+
+# {{{ _PrimaryKeys
+
+=head2 _PrimaryKeys
+
+The primary keys for RT classes is 'id'
+
+=cut
+
+sub _PrimaryKeys {
+    my $self = shift;
+    return(['id']);
+}
+
+# }}}
+
+# {{{ sub _MyCurrentUser 
+
+sub _MyCurrentUser  {
+    my $self = shift;
+  
+    $self->CurrentUser(@_);
+    if(!defined($self->CurrentUser)) {
+       use Carp;
+       Carp::cluck();
+       $RT::Logger->err("$self was created without a CurrentUser\n"); 
+      return(0);
+    }
+}
+
+# }}}
+
+# {{{ sub _Handle 
+sub _Handle  {
+  my $self = shift;
+  return($RT::Handle);
+}
+# }}}
+
+# {{{ sub Create 
+
+sub Create  {
+    my $self = shift;
+    my $now = new RT::Date($self->CurrentUser);
+    $now->Set(Format=> 'unix', Value => time);
+    push @_, 'Created', $now->ISO()
+      if ($self->_Accessible('Created', 'auto'));
+    
+
+    push @_, 'Creator', $self->{'user'}->id
+      if $self->_Accessible('Creator', 'auto');
+    
+    push @_, 'LastUpdated', $now->ISO()
+      if ($self->_Accessible('LastUpdated', 'auto'));
+
+    push @_, 'LastUpdatedBy', $self->{'user'}->id
+      if $self->_Accessible('LastUpdatedBy', 'auto');
+    
+    
+
+   my $id = $self->SUPER::Create(@_);
+    
+    if ($id) {
+       $self->Load($id);
+    }
+    
+    return($id);
+    
+}
+
+# }}}
+
+
+# {{{ sub LoadByCols
+
+=head2 LoadByCols
+
+Override DBIx::SearchBuilder::LoadByCols to do case-insensitive loads if the 
+DB is case sensitive
+
+=cut
+
+sub LoadByCols {
+    my $self = shift;
+    my %hash = (@_);
+
+    # If this database is case sensitive we need to uncase objects for
+    # explicit loading
+    if ($self->_Handle->CaseSensitive) {
+        my %newhash;
+        foreach my $key (keys %hash) {
+        # If we've been passed an empty value, we can't do the lookup. 
+               # We don't need to explicitly downcase integers or an id.
+               if ($key =~ '^id$' || $hash{$key} =~/^\d+$/ || !defined ($hash{$key}) ) {
+                       $newhash{$key} = $hash{$key};
+               }
+               else {
+                       $newhash{"lower(".$key.")"} = lc($hash{$key});  
+               }
+        }
+       $self->SUPER::LoadByCols(%newhash);
+    }
+    else {
+       $self->SUPER::LoadByCols(%hash);
+    }
+}
+
+# }}}
+
+
+# {{{ Datehandling
+
+# There is room for optimizations in most of those subs:
+
+# {{{ LastUpdatedObj
+
+sub LastUpdatedObj {
+    my $self=shift;
+    my $obj = new RT::Date($self->CurrentUser);
+    
+    $obj->Set(Format => 'sql', Value => $self->LastUpdated);
+    return $obj;
+}
+
+# }}}
+
+# {{{ CreatedObj
+
+sub CreatedObj {
+    my $self=shift;
+    my $obj = new RT::Date($self->CurrentUser);
+    
+    $obj->Set(Format => 'sql', Value => $self->Created);
+
+    
+    return $obj;
+}
+
+# }}}
+
+# {{{ AgeAsString
+#
+# TODO: This should be deprecated
+#
+sub AgeAsString {
+    my $self=shift;
+    return($self->CreatedObj->AgeAsString());
+}
+# }}}
+
+# {{{ LastUpdatedAsString
+
+# TODO this should be deprecated
+
+sub LastUpdatedAsString {
+    my $self=shift;
+    if ($self->LastUpdated) {
+       return ($self->LastUpdatedObj->AsString());
+         
+    } else {
+       return "never";
+    }
+}
+
+# }}}
+
+# {{{ CreatedAsString
+#
+# TODO This should be deprecated 
+#
+sub CreatedAsString {
+    my $self = shift;
+    return ($self->CreatedObj->AsString());
+}
+# }}}
+
+# {{{ LongSinceUpdateAsString
+#
+# TODO This should be deprecated
+#
+sub LongSinceUpdateAsString {
+    my $self=shift;
+    if ($self->LastUpdated) {
+      
+        return ($self->LastUpdatedObj->AgeAsString());
+       
+    } else {
+       return "never";
+    }
+}
+# }}}
+
+# }}} Datehandling
+
+
+# {{{ sub _Set 
+sub _Set  {
+  my $self = shift;
+
+  my %args = ( Field => undef,
+              Value => undef,
+              IsSQL => undef,
+              @_ );
+
+
+  #if the user is trying to modify the record
+  if ((!defined ($args{'Field'})) || (!defined ($args{'Value'}))) {
+    $args{'Value'} = 0; 
+   }
+
+  $self->_SetLastUpdated();
+  $self->SUPER::_Set(Field => $args{'Field'},
+                    Value => $args{'Value'},
+                    IsSQL => $args{'IsSQL'});
+  
+  
+}
+# }}}
+
+# {{{ sub _SetLastUpdated
+
+=head2 _SetLastUpdated
+
+This routine updates the LastUpdated and LastUpdatedBy columns of the row in question
+It takes no options. Arguably, this is a bug
+
+=cut
+
+sub _SetLastUpdated {
+    my $self = shift;
+    use RT::Date;
+    my $now = new RT::Date($self->CurrentUser);
+    $now->SetToNow();
+
+    if ($self->_Accessible('LastUpdated','auto')) {
+       my ($msg, $val) = $self->__Set( Field => 'LastUpdated',
+                                        Value => $now->ISO);
+    }
+    if ($self->_Accessible('LastUpdatedBy','auto')) {
+        my ($msg, $val) = $self->__Set( Field => 'LastUpdatedBy', 
+                                       Value => $self->CurrentUser->id);
+    }
+}
+
+# }}}
+
+# {{{ sub CreatorObj 
+
+=head2 CreatorObj
+
+Returns an RT::User object with the RT account of the creator of this row
+
+=cut
+
+sub CreatorObj  {
+  my $self = shift;
+  unless (exists $self->{'CreatorObj'}) {
+    
+    $self->{'CreatorObj'} = RT::User->new($self->CurrentUser);
+    $self->{'CreatorObj'}->Load($self->Creator);
+  }
+  return($self->{'CreatorObj'});
+}
+# }}}
+
+# {{{ sub LastUpdatedByObj
+
+=head2 LastUpdatedByObj
+
+  Returns an RT::User object of the last user to touch this object
+
+=cut
+
+sub LastUpdatedByObj {
+    my $self=shift;
+    unless (exists $self->{LastUpdatedByObj}) {
+       $self->{'LastUpdatedByObj'}=RT::User->new($self->CurrentUser);
+       $self->{'LastUpdatedByObj'}->Load($self->LastUpdatedBy);
+    }
+    return $self->{'LastUpdatedByObj'};
+}
+
+# }}}
+
+# {{{ sub CurrentUser 
+
+=head2 CurrentUser
+
+If called with an argument, sets the current user to that user object.
+This will affect ACL decisions, etc.  
+Returns the current user
+
+=cut
+
+sub CurrentUser  {
+  my $self = shift;
+
+  if (@_) {
+    $self->{'user'} = shift;
+  }
+  return ($self->{'user'});
+}
+# }}}
+
+
+1;
diff --git a/rt/lib/RT/Scrip.pm b/rt/lib/RT/Scrip.pm
new file mode 100755 (executable)
index 0000000..aef011c
--- /dev/null
@@ -0,0 +1,372 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Scrip.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Scrip - an RT Scrip object
+
+=head1 SYNOPSIS
+
+  use RT::Scrip;
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Scrip);
+
+=end testing
+
+=cut
+
+package RT::Scrip;
+use RT::Record;
+@ISA= qw(RT::Record);
+
+# {{{ sub _Init
+sub _Init  {
+    my $self = shift;
+    $self->{'table'} = "Scrips";
+    return ($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _Accessible 
+sub _Accessible  {
+    my $self = shift;
+    my %Cols = ( ScripAction  => 'read/write',
+                ScripCondition => 'read/write',
+                Stage => 'read/write',
+                Queue => 'read/write', 
+                Template => 'read/write',
+              );
+    return($self->SUPER::_Accessible(@_, %Cols));
+}
+# }}}
+
+# {{{ sub Create 
+
+=head2 Create
+
+Creates a new entry in the Scrips table. Takes a paramhash with the attributes:
+
+    Queue           A queue id or 0 for a global scrip
+    Template        A template ID or name.  
+                    Behavior is undefined if you have multiple items with 
+                    the same name
+    ScripAction     A ScripAction id or name
+                    Behavior is undefined if you have multiple items with 
+                    the same name
+    ScripCondition  A ScripCondition id or name
+                    Behavior is undefined if you have multiple items with 
+                    the same name
+
+Returns (retval, msg);
+retval is 0 for failure or scrip id.  msg is a textual description of what happened.
+
+=cut
+
+sub Create  {
+    my $self = shift;
+    my %args = ( Queue => undef,
+                Template => undef,
+                ScripAction => undef,
+                ScripCondition => undef,
+                Stage => 'TransactionCreate',
+                @_
+              );
+    
+      
+    if ($args{'Queue'} == 0 ) { 
+       unless ($self->CurrentUser->HasSystemRight('ModifyScrips')) {
+           return (0, 'Permission Denied');
+       }       
+    }
+    else {
+       my $QueueObj = new RT::Queue($self->CurrentUser);
+       $QueueObj->Load($args{'Queue'});
+       unless ($QueueObj->id()) {
+           return (0,'Invalid queue');
+       }
+       unless ($QueueObj->CurrentUserHasRight('ModifyScrips')) {
+           return (0, 'Permssion Denied');
+       }       
+    }
+
+    #TODO +++ validate input 
+
+    require RT::ScripAction;
+    my $action = new RT::ScripAction($self->CurrentUser);
+    $action->Load($args{'ScripAction'});
+    return (0, "Action ".$args{'ScripAction'}." not found") unless $action->Id;
+
+    require RT::Template;
+    my $template = new RT::Template($self->CurrentUser);
+    $template->Load($args{'Template'});
+    return (0, 'Template not found') unless $template->Id;
+
+    require RT::ScripCondition;
+    my $condition = new RT::ScripCondition($self->CurrentUser);
+    $condition->Load($args{'ScripCondition'});
+
+    unless ($condition->Id) {
+       return (0, 'Condition not found');
+    }  
+    
+    my $id = $self->SUPER::Create(Queue => $args{'Queue'},
+                                 Template => $template->Id,
+                                 ScripCondition => $condition->id,
+                                 Stage => $args{'Stage'},
+                                 ScripAction => $action->Id
+                                );
+    return ($id, 'Scrip Created'); 
+}
+
+# }}}
+
+# {{{ sub Delete
+
+=head2 Delete
+
+Delete this object
+
+=cut
+
+sub Delete {
+    my $self = shift;
+    
+    unless ($self->CurrentUserHasRight('ModifyScrips')) {
+       return (0, 'Permission Denied');
+    }
+    
+    return ($self->SUPER::Delete(@_));
+}
+# }}}
+
+# {{{ sub QueueObj
+
+=head2 QueueObj
+
+Retuns an RT::Queue object with this Scrip\'s queue
+
+=cut
+
+sub QueueObj {
+    my $self = shift;
+    
+    if (!$self->{'QueueObj'})  {
+       require RT::Queue;
+       $self->{'QueueObj'} = RT::Queue->new($self->CurrentUser);
+       $self->{'QueueObj'}->Load($self->Queue);
+    }
+    return ($self->{'QueueObj'});
+}
+
+# }}}
+
+# {{{ sub ActionObj
+
+
+=head2 ActionObj
+
+Retuns an RT::Action object with this Scrip\'s Action
+
+=cut
+
+sub ActionObj {
+    my $self = shift;
+    
+    unless (defined $self->{'ScripActionObj'})  {
+       require RT::ScripAction;
+       
+       $self->{'ScripActionObj'} = RT::ScripAction->new($self->CurrentUser);
+       #TODO: why are we loading Actions with templates like this. 
+       # two seperate methods might make more sense
+       $self->{'ScripActionObj'}->Load($self->ScripAction, $self->Template);
+    }
+    return ($self->{'ScripActionObj'});
+}
+
+# }}}
+
+
+# {{{ sub TemplateObj
+=head2 TemplateObj
+
+Retuns an RT::Template object with this Scrip\'s Template
+
+=cut
+
+sub TemplateObj {
+    my $self = shift;
+    
+    unless (defined $self->{'TemplateObj'})  {
+       require RT::Template;
+       $self->{'TemplateObj'} = RT::Template->new($self->CurrentUser);
+       $self->{'TemplateObj'}->Load($self->Template);
+    }
+    return ($self->{'TemplateObj'});
+}
+
+# }}}
+
+# {{{ sub Prepare
+=head2 Prepare
+
+Calls the action object's prepare method
+
+=cut
+
+sub Prepare {
+    my $self = shift;
+    $self->ActionObj->Prepare(@_);
+}
+
+# }}}
+
+# {{{ sub Commit
+=head2 Commit
+
+Calls the action object's commit method
+
+=cut
+
+sub Commit {
+    my $self = shift;
+    $self->ActionObj->Commit(@_);
+}
+
+# }}}
+
+# {{{ sub ConditionObj
+
+=head2 ConditionObj
+
+Retuns an RT::ScripCondition object with this Scrip's IsApplicable
+
+=cut
+
+sub ConditionObj {
+    my $self = shift;
+    
+    unless (defined $self->{'ScripConditionObj'})  {
+       require RT::ScripCondition;
+       $self->{'ScripConditionObj'} = RT::ScripCondition->new($self->CurrentUser);
+       $self->{'ScripConditionObj'}->Load($self->ScripCondition);
+    }
+    return ($self->{'ScripConditionObj'});
+}
+
+# }}}
+
+# {{{ sub IsApplicable
+
+=head2 IsApplicable
+
+Calls the  Condition object\'s IsApplicable method
+
+=cut
+
+sub IsApplicable {
+    my $self = shift;
+    return ($self->ConditionObj->IsApplicable(@_));
+}
+
+# }}}
+
+# {{{ sub DESTROY
+sub DESTROY {
+    my $self = shift;
+    $self->{'ActionObj'} = undef;
+}
+# }}}
+
+# {{{ ACL related methods
+
+# {{{ sub _Set
+
+# does an acl check and then passes off the call
+sub _Set {
+    my $self = shift;
+    
+    unless ($self->CurrentUserHasRight('ModifyScrips')) {
+        $RT::Logger->debug("CurrentUser can't modify Scrips for ".$self->Queue."\n");
+       return (0, 'Permission Denied');
+    }
+    return $self->__Set(@_);
+}
+
+# }}}
+
+# {{{ sub _Value
+# does an acl check and then passes off the call
+sub _Value {
+    my $self = shift;
+    
+    unless ($self->CurrentUserHasRight('ShowScrips')) {
+        $RT::Logger->debug("CurrentUser can't modify Scrips for ".$self->__Value('Queue')."\n");
+       return (undef);
+    }
+    
+    return $self->__Value(@_);
+}
+# }}}
+
+# {{{ sub CurrentUserHasRight
+
+=head2 CurrentUserHasRight
+
+Helper menthod for HasRight. Presets Principal to CurrentUser then 
+calls HasRight.
+
+=cut
+
+sub CurrentUserHasRight {
+    my $self = shift;
+    my $right = shift;
+    return ($self->HasRight( Principal => $self->CurrentUser->UserObj,
+                             Right => $right ));
+    
+}
+
+# }}}
+
+# {{{ sub HasRight
+
+=head2 HasRight
+
+Takes a param-hash consisting of "Right" and "Principal"  Principal is 
+an RT::User object or an RT::CurrentUser object. "Right" is a textual
+Right string that applies to Scrips.
+
+=cut
+
+sub HasRight {
+    my $self = shift;
+    my %args = ( Right => undef,
+                 Principal => undef,
+                 @_ );
+    
+    if ((defined $self->SUPER::_Value('Queue')) and ($self->SUPER::_Value('Queue') != 0)) {
+        return ( $args{'Principal'}->HasQueueRight(
+                                                  Right => $args{'Right'},
+                                                  Queue => $self->SUPER::_Value('Queue'),
+                                                  Principal => $args{'Principal'}
+                                                 ) 
+              );
+       
+    }
+    else {
+        return( $args{'Principal'}->HasSystemRight( $args{'Right'}) );
+    }
+}
+# }}}
+
+# }}}
+
+1;
+
+
diff --git a/rt/lib/RT/ScripAction.pm b/rt/lib/RT/ScripAction.pm
new file mode 100755 (executable)
index 0000000..471ad91
--- /dev/null
@@ -0,0 +1,200 @@
+# Copyright 1999-2000 Jesse Vincent <jesse@fsck.com>
+# Released under the terms of the GNU Public License
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/ScripAction.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::ScripAction - RT Action object
+
+=head1 SYNOPSIS
+
+  use RT::ScripAction;
+
+
+=head1 DESCRIPTION
+
+This module should never be called directly by client code. it's an internal module which
+should only be accessed through exported APIs in other modules.
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::ScripAction);
+
+=end testing
+
+=head1 METHODS
+
+=cut
+
+package RT::ScripAction;
+use RT::Record;
+@ISA= qw(RT::Record);
+
+# {{{  sub _Init 
+sub _Init  {
+    my $self = shift; 
+    $self->{'table'} = "ScripActions";
+    return ($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _Accessible 
+sub _Accessible  {
+    my $self = shift;
+    my %Cols = ( Name  => 'read',
+                Description => 'read',
+                ExecModule  => 'read',
+                Argument  => 'read',
+                Creator => 'read/auto',
+                Created => 'read/auto',
+                LastUpdatedBy => 'read/auto',
+                LastUpdated => 'read/auto'
+       );
+    return($self->SUPER::_Accessible(@_, %Cols));
+}
+# }}}
+
+# {{{ sub Create 
+=head2 Create
+  
+ Takes a hash. Creates a new Action entry.
+ should be better documented.
+=cut
+
+sub Create  {
+    my $self = shift;
+    #TODO check these args and do smart things.
+    return($self->SUPER::Create(@_));
+}
+# }}}
+
+# {{{ sub Delete 
+sub Delete  {
+    my $self = shift;
+    
+    return (0, "ScripAction->Delete not implemented");
+}
+# }}}
+
+# {{{ sub Load 
+sub Load  {
+    my $self = shift;
+    my $identifier = shift;
+    
+    if (!$identifier) {
+       return (0, 'Input error');
+    }      
+    
+    if ($identifier !~ /\D/) {
+       $self->SUPER::LoadById($identifier);
+    }
+    else {
+       $self->LoadByCol('Name', $identifier);
+       
+    }
+
+    if (@_) {
+       # Set the template Id to the passed in template    
+       my $template = shift;
+       
+       $self->{'Template'} = $template;
+    }
+    return ($self->Id, 'ScripAction loaded');
+}
+# }}}
+
+# {{{ sub LoadAction 
+
+=head2 LoadAction HASH
+
+  Takes a hash consisting of TicketObj and TransactionObj.  Loads an RT::Action:: module.
+
+=cut
+
+sub LoadAction  {
+    my $self = shift;
+    my %args = ( TransactionObj => undef,
+                TicketObj => undef,
+                @_ );
+    
+    #TODO: Put this in an eval  
+    $self->ExecModule =~ /^(\w+)$/;
+    my $module = $1;
+    my $type = "RT::Action::". $module;
+    $RT::Logger->debug("now requiring $type\n"); 
+    eval "require $type" || die "Require of $type failed.\n$@\n";
+    
+    $self->{'Action'}  = $type->new ( 'ScripActionObj' => $self, 
+                                     'TicketObj' => $args{'TicketObj'},
+                                     'TransactionObj' => $args{'TransactionObj'},
+                                     'TemplateObj' => $self->TemplateObj,
+                                     'Argument' => $self->Argument,
+                                   );
+}
+# }}}
+
+# {{{ sub TemplateObj
+
+=head2 TemplateObj
+
+Return this action\'s template object
+
+=cut
+
+sub TemplateObj {
+    my $self = shift;
+    return undef unless $self->{Template};
+    if (!$self->{'TemplateObj'})  {
+       require RT::Template;
+       $self->{'TemplateObj'} = RT::Template->new($self->CurrentUser);
+       $self->{'TemplateObj'}->LoadById($self->{'Template'});
+       
+    }
+    
+    return ($self->{'TemplateObj'});
+}
+# }}}
+
+# The following methods call the action object
+
+# {{{ sub Prepare 
+
+sub Prepare  {
+    my $self = shift;
+    return ($self->{'Action'}->Prepare());
+  
+}
+# }}}
+
+# {{{ sub Commit 
+sub Commit  {
+    my $self = shift;
+    return($self->{'Action'}->Commit());
+    
+    
+}
+# }}}
+
+# {{{ sub Describe 
+sub Describe  {
+    my $self = shift;
+    return ($self->{'Action'}->Describe());
+    
+}
+# }}}
+
+# {{{ sub DESTROY
+sub DESTROY {
+    my $self=shift;
+    $self->{'Action'} = undef;
+    $self->{'TemplateObj'} = undef;
+}
+# }}}
+
+
+1;
+
+
diff --git a/rt/lib/RT/ScripActions.pm b/rt/lib/RT/ScripActions.pm
new file mode 100755 (executable)
index 0000000..ec61415
--- /dev/null
@@ -0,0 +1,70 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/ScripActions.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::ScripActions - Collection of Action objects
+
+=head1 SYNOPSIS
+
+  use RT::ScripActions;
+
+
+=head1 DESCRIPTION
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::ScripActions);
+
+=end testing
+
+=head1 METHODS
+
+=cut
+
+package RT::ScripActions;
+use RT::EasySearch;
+use RT::ScripAction;
+
+@ISA= qw(RT::EasySearch);
+
+# {{{ sub _Init
+sub _Init { 
+  my $self = shift;
+  $self->{'table'} = "ScripActions";
+  $self->{'primary_key'} = "id";
+  return ( $self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub LimitToType 
+sub LimitToType  {
+  my $self = shift;
+  my $type = shift;
+  $self->Limit (ENTRYAGGREGATOR => 'OR',
+               FIELD => 'Type',
+               VALUE => "$type")
+      if defined $type;
+  $self->Limit (ENTRYAGGREGATOR => 'OR',
+               FIELD => 'Type',
+               VALUE => "Correspond")
+      if $type eq "Create";
+  $self->Limit (ENTRYAGGREGATOR => 'OR',
+               FIELD => 'Type',
+               VALUE => 'any');
+  
+}
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem  {
+  my $self = shift;
+  return(RT::ScripAction->new($self->CurrentUser));
+
+}
+# }}}
+
+
+1;
+
diff --git a/rt/lib/RT/ScripCondition.pm b/rt/lib/RT/ScripCondition.pm
new file mode 100755 (executable)
index 0000000..253502b
--- /dev/null
@@ -0,0 +1,192 @@
+# Copyright 1999-2000 Jesse Vincent <jesse@fsck.com>
+# Released under the terms of the GNU Public License
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/ScripCondition.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::ScripCondition - RT scrip conditional
+
+=head1 SYNOPSIS
+
+  use RT::ScripCondition;
+
+
+=head1 DESCRIPTION
+
+This module should never be called directly by client code. it's an internal module which
+should only be accessed through exported APIs in other modules.
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::ScripCondition);
+
+=end testing
+
+=head1 METHODS
+
+=cut
+
+package RT::ScripCondition;
+use RT::Record;
+@ISA= qw(RT::Record);
+
+# {{{  sub _Init 
+sub _Init  {
+    my $self = shift; 
+    $self->{'table'} = "ScripConditions";
+    return ($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _Accessible 
+sub _Accessible  {
+    my $self = shift;
+    my %Cols = ( Name  => 'read',
+                Description => 'read',
+                ApplicableTransTypes    => 'read',
+                ExecModule  => 'read',
+                Argument  => 'read',
+                Creator => 'read/auto',
+                Created => 'read/auto',
+                LastUpdatedBy => 'read/auto',
+                LastUpdated => 'read/auto'
+              );
+    return($self->SUPER::_Accessible(@_, %Cols));
+}
+# }}}
+
+# {{{ sub Create 
+
+=head2 Create
+  
+  Takes a hash. Creates a new Condition entry.
+  should be better documented.
+
+=cut
+
+sub Create  {
+    my $self = shift;
+    return($self->SUPER::Create(@_));
+}
+# }}}
+
+# {{{ sub Delete 
+
+=head2 Delete
+
+No API available for deleting things just yet.
+
+=cut
+
+sub Delete  {
+    my $self = shift;
+    return(0,'Unimplemented');
+}
+# }}}
+
+# {{{ sub Load 
+
+=head2 Load IDENTIFIER
+
+Loads a condition takes a name or ScripCondition id.
+
+=cut
+
+sub Load  {
+    my $self = shift;
+    my $identifier = shift;
+    
+    unless (defined $identifier) {
+       return (undef);
+    }      
+    
+    if ($identifier !~ /\D/) {
+       return ($self->SUPER::LoadById($identifier));
+    }
+    else {
+       return ($self->LoadByCol('Name', $identifier));
+    }
+}
+# }}}
+
+# {{{ sub LoadCondition 
+
+=head2 LoadCondition  HASH
+
+takes a hash which has the following elements:  TransactionObj and TicketObj.
+Loads the Condition module in question.
+
+=cut
+
+
+sub LoadCondition  {
+    my $self = shift;
+    my %args = ( TransactionObj => undef,
+                TicketObj => undef,
+                @_ );
+    
+    #TODO: Put this in an eval  
+    $self->ExecModule =~ /^(\w+)$/;
+    my $module = $1;
+    my $type = "RT::Condition::". $module;
+    
+    $RT::Logger->debug("now requiring $type\n"); 
+    eval "require $type" || die "Require of $type failed.\n$@\n";
+    
+    $self->{'Condition'}  = $type->new ( 'ScripConditionObj' => $self, 
+                                        'TicketObj' => $args{'TicketObj'},
+                                        'TransactionObj' => $args{'TransactionObj'},
+                                        'Argument' => $self->Argument,
+                                        'ApplicableTransTypes' => $self->ApplicableTransTypes,
+                                      );
+}
+# }}}
+
+# {{{ The following methods call the Condition object
+
+
+# {{{ sub Describe 
+
+=head2 Describe 
+
+Helper method to call the condition module\'s Describe method.
+
+=cut
+
+sub Describe  {
+    my $self = shift;
+    return ($self->{'Condition'}->Describe());
+    
+}
+# }}}
+
+# {{{ sub IsApplicable 
+
+=head2 IsApplicable
+
+Helper method to call the condition module\'s IsApplicable method.
+
+=cut
+
+sub IsApplicable  {
+    my $self = shift;
+    return ($self->{'Condition'}->IsApplicable());
+    
+}
+# }}}
+
+# }}}
+
+# {{{ sub DESTROY
+sub DESTROY {
+    my $self=shift;
+    $self->{'Condition'} = undef;
+}
+# }}}
+
+
+1;
+
+
diff --git a/rt/lib/RT/ScripConditions.pm b/rt/lib/RT/ScripConditions.pm
new file mode 100755 (executable)
index 0000000..236e671
--- /dev/null
@@ -0,0 +1,69 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/ScripConditions.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::ScripConditions - Collection of Action objects
+
+=head1 SYNOPSIS
+
+  use RT::ScripConditions;
+
+
+=head1 DESCRIPTION
+
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::ScripConditions);
+
+=end testing
+
+=head1 METHODS
+
+=cut
+
+package RT::ScripConditions;
+use RT::EasySearch;
+use RT::ScripCondition;
+@ISA= qw(RT::EasySearch);
+
+# {{{ sub _Init
+sub _Init { 
+  my $self = shift;
+  $self->{'table'} = "ScripConditions";
+  $self->{'primary_key'} = "id";
+  return ( $self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub LimitToType 
+sub LimitToType  {
+  my $self = shift;
+  my $type = shift;
+  $self->Limit (ENTRYAGGREGATOR => 'OR',
+               FIELD => 'Type',
+               VALUE => "$type")
+      if defined $type;
+  $self->Limit (ENTRYAGGREGATOR => 'OR',
+               FIELD => 'Type',
+               VALUE => "Correspond")
+      if $type eq "Create";
+  $self->Limit (ENTRYAGGREGATOR => 'OR',
+               FIELD => 'Type',
+               VALUE => 'any');
+  
+}
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem  {
+  my $self = shift;
+  return(RT::ScripCondition->new($self->CurrentUser));
+}
+# }}}
+
+
+1;
+
diff --git a/rt/lib/RT/Scrips.pm b/rt/lib/RT/Scrips.pm
new file mode 100755 (executable)
index 0000000..90be847
--- /dev/null
@@ -0,0 +1,127 @@
+# Copyright 1999-2001 Jesse Vincent <jesse@fsck.com>
+# Released under the terms of the GNU Public License
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Scrips.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Scrips - a collection of RT Scrip objects
+
+=head1 SYNOPSIS
+
+  use RT::Scrips;
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Scrips);
+
+=end testing
+
+=cut
+
+package RT::Scrips;
+use RT::EasySearch;
+use RT::Scrip;
+@ISA= qw(RT::EasySearch);
+
+
+# {{{ sub _Init
+sub _Init { 
+  my $self = shift;
+  $self->{'table'} = "Scrips";
+  $self->{'primary_key'} = "id";
+  return ( $self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub LimitToQueue 
+
+=head2 LimitToQueue
+
+Takes a queue id (numerical) as its only argument. Makes sure that 
+Scopes it pulls out apply to this queue (or another that you've selected with
+another call to this method
+
+=cut
+
+sub LimitToQueue  {
+   my $self = shift;
+  my $queue = shift;
+  $self->Limit (ENTRYAGGREGATOR => 'OR',
+               FIELD => 'Queue',
+               VALUE => "$queue")
+      if defined $queue;
+  
+}
+# }}}
+
+# {{{ sub LimitToGlobal
+
+=head2 LimitToGlobal
+
+Makes sure that 
+Scopes it pulls out apply to all queues (or another that you've selected with
+another call to this method or LimitToQueue
+
+=cut
+
+
+sub LimitToGlobal  {
+   my $self = shift;
+  $self->Limit (ENTRYAGGREGATOR => 'OR',
+               FIELD => 'Queue',
+               VALUE => 0);
+  
+}
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem  {
+  my $self = shift;
+  
+  return(new RT::Scrip($self->CurrentUser));
+}
+# }}}
+
+# {{{ sub Next 
+
+=head2 Next
+
+Returns the next scrip that this user can see.
+
+=cut
+  
+sub Next {
+    my $self = shift;
+    
+    
+    my $Scrip = $self->SUPER::Next();
+    if ((defined($Scrip)) and (ref($Scrip))) {
+
+       if ($Scrip->CurrentUserHasRight('ShowScrips')) {
+           return($Scrip);
+       }
+       
+       #If the user doesn't have the right to show this scrip
+       else {  
+           return($self->Next());
+       }
+    }
+    #if there never was any scrip
+    else {
+       return(undef);
+    }  
+    
+}
+# }}}
+
+1;
+
diff --git a/rt/lib/RT/Template.pm b/rt/lib/RT/Template.pm
new file mode 100755 (executable)
index 0000000..3ef96c7
--- /dev/null
@@ -0,0 +1,395 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Template.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Copyright 1996-2002 Jesse Vincent <jesse@bestpractical.com>
+# Portions Copyright 2000 Tobias Brox <tobix@cpan.org> 
+# Released under the terms of the GNU General Public License
+
+=head1 NAME
+
+  RT::Template - RT's template object
+
+=head1 SYNOPSIS
+
+  use RT::Template;
+
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::Template);
+
+=end testing
+
+=cut
+
+package RT::Template;
+use RT::Record;
+use MIME::Entity;
+use MIME::Parser;
+
+@ISA = qw(RT::Record);
+
+# {{{ sub _Init
+
+sub _Init {
+    my $self = shift;
+    $self->{'table'} = "Templates";
+    return ( $self->SUPER::_Init(@_) );
+}
+
+# }}}
+
+# {{{ sub _Accessible 
+
+sub _Accessible {
+    my $self = shift;
+    my %Cols = (
+        id            => 'read',
+        Name          => 'read/write',
+        Description   => 'read/write',
+        Type          => 'read/write',    #Type is one of Action or Message
+        Content       => 'read/write',
+        Queue         => 'read/write',
+        Creator       => 'read/auto',
+        Created       => 'read/auto',
+        LastUpdatedBy => 'read/auto',
+        LastUpdated   => 'read/auto'
+    );
+    return $self->SUPER::_Accessible( @_, %Cols );
+}
+
+# }}}
+
+# {{{ sub _Set
+
+sub _Set {
+    my $self = shift;
+
+    # use super::value or we get acl blocked
+    if ( ( defined $self->SUPER::_Value('Queue') )
+        && ( $self->SUPER::_Value('Queue') == 0 ) )
+    {
+        unless ( $self->CurrentUser->HasSystemRight('ModifyTemplate') ) {
+            return ( 0, 'Permission Denied' );
+        }
+    }
+    else {
+
+        unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
+            return ( 0, 'Permission Denied' );
+        }
+    }
+    return ( $self->SUPER::_Set(@_) );
+
+}
+
+# }}}
+
+# {{{ sub _Value 
+
+=head2 _Value
+
+Takes the name of a table column.
+Returns its value as a string, if the user passes an ACL check
+
+=cut
+
+sub _Value {
+
+    my $self  = shift;
+    my $field = shift;
+
+    #If the current user doesn't have ACLs, don't let em at it.  
+    #use super::value or we get acl blocked
+    if ( ( !defined $self->__Value('Queue') )
+        || ( $self->__Value('Queue') == 0 ) )
+    {
+        unless ( $self->CurrentUser->HasSystemRight('ShowTemplate') ) {
+            return (undef);
+        }
+    }
+    else {
+        unless ( $self->CurrentUserHasQueueRight('ShowTemplate') ) {
+            return (undef);
+        }
+    }
+    return ( $self->__Value($field) );
+
+}
+
+# }}}
+
+# {{{ sub Load
+
+=head2 Load <identifer>
+
+Load a template, either by number or by name
+
+=cut
+
+sub Load {
+    my $self       = shift;
+    my $identifier = shift;
+
+    if ( !$identifier ) {
+        return (undef);
+    }
+
+    if ( $identifier !~ /\D/ ) {
+        $self->SUPER::LoadById($identifier);
+    }
+    else {
+        $self->LoadByCol( 'Name', $identifier );
+
+    }
+}
+
+# }}}
+
+# {{{ sub LoadGlobalTemplate
+
+=head2 LoadGlobalTemplate NAME
+
+Load the global tempalte with the name NAME
+
+=cut
+
+sub LoadGlobalTemplate {
+    my $self = shift;
+    my $id   = shift;
+
+    return ( $self->LoadQueueTemplate( Queue => 0, Name => $id ) );
+}
+
+# }}}
+
+# {{{ sub LoadQueueTemplate
+
+=head2  LoadQueueTemplate (Queue => QUEUEID, Name => NAME)
+
+Loads the Queue template named NAME for Queue QUEUE.
+
+=cut
+
+sub LoadQueueTemplate {
+    my $self = shift;
+    my %args = (
+        Queue => undef,
+        Name  => undef
+    );
+
+    return ( $self->LoadByCols( Name => $args{'Name'}, Queue => {'Queue'} ) );
+
+}
+
+# }}}
+
+# {{{ sub Create
+
+=head2 Create
+
+Takes a paramhash of Content, Queue, Name and Description.
+Name should be a unique string identifying this Template.
+Description and Content should be the template's title and content.
+Queue should be 0 for a global template and the queue # for a queue-specific 
+template.
+
+Returns the Template's id # if the create was successful. Returns undef for
+unknown database failure.
+
+
+=cut
+
+sub Create {
+    my $self = shift;
+    my %args = (
+        Content     => undef,
+        Queue       => 0,
+        Description => '[no description]',
+        Type => 'Action',    #By default, template are 'Action' templates
+        Name => undef,
+        @_
+    );
+
+    if ( $args{'Queue'} == 0 ) {
+        unless ( $self->CurrentUser->HasSystemRight('ModifyTemplate') ) {
+            return (undef);
+        }
+    }
+    else {
+        my $QueueObj = new RT::Queue( $self->CurrentUser );
+        $QueueObj->Load( $args{'Queue'} ) || return ( 0, 'Invalid queue' );
+
+        unless ( $QueueObj->CurrentUserHasRight('ModifyTemplate') ) {
+            return (undef);
+        }
+    }
+
+    my $result = $self->SUPER::Create(
+        Content => $args{'Content'},
+        Queue   => $args{'Queue'},
+        ,
+        Description => $args{'Description'},
+        Name        => $args{'Name'}
+    );
+
+    return ($result);
+
+}
+
+# }}}
+
+# {{{ sub Delete
+
+=head2 Delete
+
+Delete this template.
+
+=cut
+
+sub Delete {
+    my $self = shift;
+
+    unless ( $self->CurrentUserHasRight('ModifyTemplate') ) {
+        return ( 0, 'Permission Denied' );
+    }
+
+    return ( $self->SUPER::Delete(@_) );
+}
+
+# }}}
+
+# {{{ sub MIMEObj
+sub MIMEObj {
+    my $self = shift;
+    return ( $self->{'MIMEObj'} );
+}
+
+# }}}
+
+# {{{ sub Parse 
+
+=item Parse
+
+ This routine performs Text::Template parsing on thte template and then imports the 
+ results into a MIME::Entity so we can really use it
+ It returns a tuple of (val, message)
+ If val is 0, the message contains an error message
+
+=cut
+
+sub Parse {
+    my $self = shift;
+
+    #We're passing in whatever we were passed. it's destined for _ParseContent
+    my $content = $self->_ParseContent(@_);
+
+    #Lets build our mime Entity
+
+    my $parser = MIME::Parser->new();
+    
+    # Do work on the parsed template in memory, rather than on disk
+    $parser->output_to_core(1); 
+
+    ### Should we forgive normally-fatal errors?
+    $parser->ignore_errors(1);
+    $self->{'MIMEObj'} = eval { $parser->parse_data($content) };
+    $error = ( $@ || $parser->last_error );
+
+    if ($error) {
+        $RT::Logger->error("$error");
+        return ( 0, $error );
+    }
+
+    # Unfold all headers
+    $self->{'MIMEObj'}->head->unfold();
+
+    return ( 1, "Template parsed" );
+   
+
+}
+
+# }}}
+
+# {{{ sub _ParseContent
+
+# Perform Template substitutions on the template
+
+sub _ParseContent {
+    my $self = shift;
+    my %args = (
+        Argument       => undef,
+        TicketObj      => undef,
+        TransactionObj => undef,
+        @_
+    );
+
+    # Might be subject to change
+    use Text::Template;
+
+    $T::Ticket      = $args{'TicketObj'};
+    $T::Transaction = $args{'TransactionObj'};
+    $T::Argument    = $args{'Argument'};
+    $T::rtname      = $RT::rtname;
+
+    # We need to untaint the content of the template, since we'll be working
+    # with it
+    my $content = $self->Content();
+    $content =~ s/^(.*)$/$1/;
+    $template = Text::Template->new(
+        TYPE   => STRING,
+        SOURCE => $content
+    );
+
+    my $retval = $template->fill_in( PACKAGE => T );
+    return ($retval);
+}
+
+# }}}
+
+# {{{ sub QueueObj
+
+=head2 QueueObj
+
+Takes nothing. returns this ticket's queue object
+
+=cut
+
+sub QueueObj {
+    my $self = shift;
+    if ( !defined $self->{'queue'} ) {
+        require RT::Queue;
+        $self->{'queue'} = RT::Queue->new( $self->CurrentUser );
+
+        unless ( $self->{'queue'} ) {
+            $RT::Logger->crit(
+                "RT::Queue->new(" . $self->CurrentUser . ") returned false" );
+            return (undef);
+        }
+        my ($result) = $self->{'queue'}->Load( $self->__Value('Queue') );
+
+    }
+    return ( $self->{'queue'} );
+}
+
+# }}}
+
+# {{{ sub CurrentUserHasQueueRight
+
+=head2 CurrentUserHasQueueRight
+
+Helper function to call the template's queue's CurrentUserHasQueueRight with the passed in args.
+
+=cut
+
+sub CurrentUserHasQueueRight {
+    my $self = shift;
+    return ( $self->QueueObj->CurrentUserHasRight(@_) );
+}
+
+# }}}
+1;
diff --git a/rt/lib/RT/Templates.pm b/rt/lib/RT/Templates.pm
new file mode 100755 (executable)
index 0000000..b5b483c
--- /dev/null
@@ -0,0 +1,122 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Templates.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Templates - a collection of RT Template objects
+
+=head1 SYNOPSIS
+
+  use RT::Templates;
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Templates);
+
+=end testing
+
+=cut
+
+package RT::Templates;
+use RT::EasySearch;
+@ISA= qw(RT::EasySearch);
+
+
+# {{{ sub _Init
+
+=head2 _Init
+
+  Returns RT::Templates specific init info like table and primary key names
+
+=cut
+
+sub _Init {
+    
+    my $self = shift;
+    $self->{'table'} = "Templates";
+    $self->{'primary_key'} = "id";
+    return ($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ LimitToNotInQueue
+
+=head2 LimitToNotInQueue
+
+Takes a queue id # and limits the returned set of templates to those which 
+aren't that queue's templates.
+
+=cut
+
+sub LimitToNotInQueue {
+    my $self = shift;
+    my $queue_id = shift;
+    $self->Limit(FIELD => 'Queue',
+                 VALUE => "$queue_id",
+                 OPERATOR => '!='
+                );
+}
+# }}}
+
+# {{{ LimitToGlobal
+
+=head2 LimitToGlobal
+
+Takes no arguments. Limits the returned set to "Global" templates
+which can be used with any queue.
+
+=cut
+
+sub LimitToGlobal {
+    my $self = shift;
+    my $queue_id = shift;
+    $self->Limit(FIELD => 'Queue',
+                 VALUE => "0",
+                 OPERATOR => '='
+                );
+}
+# }}}
+
+# {{{ LimitToQueue
+
+=head2 LimitToQueue
+
+Takes a queue id # and limits the returned set of templates to that queue's
+templates
+
+=cut
+
+sub LimitToQueue {
+    my $self = shift;
+    my $queue_id = shift;
+    $self->Limit(FIELD => 'Queue',
+                 VALUE => "$queue_id",
+                 OPERATOR => '='
+                );
+}
+# }}}
+
+# {{{ sub NewItem 
+
+=head2 NewItem
+
+Returns a new empty Template object
+
+=cut
+
+sub NewItem  {
+  my $self = shift;
+
+  use RT::Template;
+  my $item = new RT::Template($self->CurrentUser);
+  return($item);
+}
+# }}}
+
+1;
+
diff --git a/rt/lib/RT/TestHarness.pm b/rt/lib/RT/TestHarness.pm
new file mode 100644 (file)
index 0000000..160e9e6
--- /dev/null
@@ -0,0 +1,14 @@
+use lib "/opt/rt2/etc/";
+
+use RT::Interface::CLI  qw(CleanEnv LoadConfig DBConnect 
+                          GetCurrentUser GetMessageContent);
+
+#Clean out all the nasties from the environment
+CleanEnv();
+
+#Load etc/config.pm and drop privs
+LoadConfig();
+
+
+use RT;
+RT::Init;
diff --git a/rt/lib/RT/Ticket.pm b/rt/lib/RT/Ticket.pm
new file mode 100755 (executable)
index 0000000..f7275e4
--- /dev/null
@@ -0,0 +1,3004 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Ticket.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-2001 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+#
+
+=head1 NAME
+
+  RT::Ticket - RT ticket object
+
+=head1 SYNOPSIS
+
+  use RT::Ticket;
+  my $ticket = new RT::Ticket($CurrentUser);
+  $ticket->Load($ticket_id);
+
+=head1 DESCRIPTION
+
+This module lets you manipulate RT\'s ticket object.
+
+
+=head1 METHODS
+
+=cut
+
+
+
+package RT::Ticket;
+use RT::Queue;
+use RT::User;
+use RT::Record;
+use RT::Link;
+use RT::Links;
+use RT::Date;
+use RT::Watcher;
+
+
+@ISA= qw(RT::Record);
+
+
+=begin testing
+
+use RT::TestHarness;
+
+ok(require RT::Ticket, "Loading the RT::Ticket library");
+
+=end testing
+
+=cut
+
+# {{{ sub _Init
+
+sub _Init {
+    my $self = shift;
+    $self->{'table'} = "Tickets";
+    return ($self->SUPER::_Init(@_));
+}
+
+# }}}
+
+# {{{ sub Load
+
+=head2 Load
+
+Takes a single argument. This can be a ticket id, ticket alias or 
+local ticket uri.  If the ticket can't be loaded, returns undef.
+Otherwise, returns the ticket id.
+
+=cut
+
+sub Load {
+   my $self = shift;
+   my $id = shift;
+
+   #TODO modify this routine to look at EffectiveId and do the recursive load
+   # thing. be careful to cache all the interim tickets we try so we don't loop forever.
+   
+   #If it's a local URI, turn it into a ticket id
+   if ($id =~ /^$RT::TicketBaseURI(\d+)$/)  {
+       $id = $1;
+   }
+   #If it's a remote URI, we're going to punt for now
+   elsif ($id =~ '://' ) {
+       return (undef);
+   }
+   
+   #If we have an integer URI, load the ticket
+   if ( $id =~ /^\d+$/ ) {
+       my $ticketid = $self->LoadById($id);
+   
+       unless ($ticketid) {
+          $RT::Logger->debug("$self tried to load a bogus ticket: $id\n");
+          return(undef);
+       }
+   }
+   
+   #It's not a URI. It's not a numerical ticket ID. Punt!
+   else {
+       return(undef);
+   }
+   
+   #If we're merged, resolve the merge.
+   if (($self->EffectiveId) and
+       ($self->EffectiveId != $self->Id)) {
+          return ($self->Load($self->EffectiveId));
+       }
+
+   #Ok. we're loaded. lets get outa here.
+   return ($self->Id);
+   
+}
+
+# }}}
+
+# {{{ sub LoadByURI
+
+=head2 LoadByURI
+
+Given a local ticket URI, loads the specified ticket.
+
+=cut
+
+sub LoadByURI {
+    my $self = shift;
+    my $uri = shift;
+    
+    if ($uri =~ /^$RT::TicketBaseURI(\d+)$/) {
+        my $id = $1;
+        return ($self->Load($id));
+    }
+    else {
+        return(undef);
+    }
+}
+
+# }}}
+
+# {{{ sub Create
+
+=head2 Create (ARGS)
+
+Arguments: ARGS is a hash of named parameters.  Valid parameters are:
+
+  Queue  - Either a Queue object or a Queue Name
+  Requestor -  A reference to a list of RT::User objects, email addresses or RT user Names
+  Cc  - A reference to a list of RT::User objects, email addresses or Names
+  AdminCc  - A reference to a  list of RT::User objects, email addresses or Names
+  Type -- The ticket\'s type. ignore this for now
+  Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
+  Subject -- A string describing the subject of the ticket
+  InitialPriority -- an integer from 0 to 99
+  FinalPriority -- an integer from 0 to 99
+  Status -- any valid status (Defined in RT::Queue)
+  TimeWorked -- an integer
+  TimeLeft -- an integer
+  Starts -- an ISO date describing the ticket\'s start date and time in GMT
+  Due -- an ISO date describing the ticket\'s due date and time in GMT
+  MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
+
+  KeywordSelect-<id> -- an array of keyword ids for that keyword select
+
+
+Returns: TICKETID, Transaction Object, Error Message
+
+
+=begin testing
+
+my $t = RT::Ticket->new($RT::SystemUser);
+
+ok( $t->Create(Queue => 'General', Subject => 'This is a subject'), "Ticket Created");
+
+ok ( my $id = $t->Id, "Got ticket id");
+
+=end testing
+
+=cut
+
+sub Create {
+    my $self = shift;
+    
+    my %args = (
+               Queue => undef,
+               Requestor => undef,
+               Cc => undef,
+               AdminCc => undef,
+               Type => 'ticket',
+               Owner => $RT::Nobody->UserObj,
+               Subject => '[no subject]',
+               InitialPriority => undef,
+               FinalPriority => undef,
+               Status => 'new',
+               TimeWorked => "0",
+               TimeLeft => 0,
+               Due => undef,
+               Starts => undef,
+               MIMEObj => undef,
+               @_);
+
+    my ($ErrStr, $QueueObj, $Owner, $resolved);
+    my (@non_fatal_errors);
+    
+    my $now = RT::Date->new($self->CurrentUser);
+    $now->SetToNow();
+
+    if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) {
+       $QueueObj=RT::Queue->new($RT::SystemUser);
+       $QueueObj->Load($args{'Queue'});
+    }
+    elsif (ref($args{'Queue'}) eq 'RT::Queue') {
+       $QueueObj=RT::Queue->new($RT::SystemUser);
+       $QueueObj->Load($args{'Queue'}->Id);
+    }
+    else {
+       $RT::Logger->debug("$self ". $args{'Queue'} . 
+                        " not a recognised queue object.");
+    }
+  
+    #Can't create a ticket without a queue.
+    unless (defined ($QueueObj)) {
+       $RT::Logger->debug( "$self No queue given for ticket creation.");
+       return (0, 0,'Could not create ticket. Queue not set');
+    }
+    
+    #Now that we have a queue, Check the ACLS
+    unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket',
+                                             QueueObj => $QueueObj )) {
+       return (0,0,"No permission to create tickets in the queue '". 
+               $QueueObj->Name."'.");
+    }
+    
+    #Since we have a queue, we can set queue defaults
+    #Initial Priority
+
+    # If there's no queue default initial priority and it's not set, set it to 0
+    $args{'InitialPriority'} = ($QueueObj->InitialPriority || 0)
+      unless (defined $args{'InitialPriority'});
+       
+    #Final priority 
+
+    # If there's no queue default final priority and it's not set, set it to 0
+    $args{'FinalPriority'} = ($QueueObj->FinalPriority  || 0)
+      unless (defined $args{'FinalPriority'});
+    
+    
+    #TODO we should see what sort of due date we're getting, rather +
+    # than assuming it's in ISO format.
+    
+    #Set the due date. if we didn't get fed one, use the queue default due in
+    my $due = new RT::Date($self->CurrentUser);
+    if (defined $args{'Due'}) {
+       $due->Set (Format => 'ISO',
+                  Value => $args{'Due'});
+    }  
+    elsif (defined ($QueueObj->DefaultDueIn)) {
+       $due->SetToNow;
+       $due->AddDays($QueueObj->DefaultDueIn);
+    }  
+    
+    my $starts = new RT::Date($self->CurrentUser);
+    if (defined $args{'Starts'}) {
+       $starts->Set (Format => 'ISO',
+                  Value => $args{'Starts'});
+    }
+
+       
+    # {{{ Deal with setting the owner
+    
+    if (ref($args{'Owner'}) eq 'RT::User') {
+       $Owner = $args{'Owner'};
+    }
+    #If we've been handed something else, try to load the user.
+    elsif ($args{'Owner'}) {
+       $Owner = new RT::User($self->CurrentUser);
+       $Owner->Load($args{'Owner'});
+       
+    }
+    #If we can't handle it, call it nobody
+    else {
+       if (ref($args{'Owner'})) {
+           $RT::Logger->warning("$ticket ->Create called with an Owner of ".
+                "type ".ref($args{'Owner'}) .". Defaulting to nobody.\n");
+
+           push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'.";
+       }
+       else { 
+           $RT::Logger->warning("$self ->Create called with an ".
+                                "unknown datatype for Owner: ".$args{'Owner'} .
+                                ". Defaulting to Nobody.\n");
+       }
+    }
+    
+    #If we have a proposed owner and they don't have the right 
+    #to own a ticket, scream about it and make them not the owner
+    if ((defined ($Owner)) and
+       ($Owner->Id != $RT::Nobody->Id) and 
+       (!$Owner->HasQueueRight( QueueObj => $QueueObj, 
+                                Right => 'OwnTicket'))) {
+       
+       $RT::Logger->warning("$self user ".$Owner->Name . "(".$Owner->id .
+                            ") was proposed ".
+                            "as a ticket owner but has no rights to own ".
+                            "tickets in this queue\n");
+
+       push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'.";
+
+       $Owner = undef;
+    }
+    
+    #If we haven't been handed a valid owner, make it nobody.
+    unless (defined ($Owner)) {
+       $Owner = new RT::User($self->CurrentUser);
+       $Owner->Load($RT::Nobody->UserObj->Id);
+    }  
+
+    # }}}
+
+    unless ($self->ValidateStatus($args{'Status'})) {
+       return (0,0,'Invalid value for status');
+    }
+
+    if ($args{'Status'} eq 'resolved') {
+       $resolved = $now->ISO;
+    } else{
+       $resolved = undef;
+    }
+
+    my $id = $self->SUPER::Create(
+                                 Queue => $QueueObj->Id,
+                                 Owner => $Owner->Id,
+                                 Subject => $args{'Subject'},
+                                 InitialPriority => $args{'InitialPriority'},
+                                 FinalPriority => $args{'FinalPriority'},
+                                 Priority => $args{'InitialPriority'},
+                                 Status => $args{'Status'},
+                                 TimeWorked => $args{'TimeWorked'},
+                                 TimeLeft => $args{'TimeLeft'},
+                                 Type => $args{'Type'},        
+                                 Starts => $starts->ISO,
+                                 Resolved => $resolved,
+                                 Due => $due->ISO
+                                );
+    #Set the ticket's effective ID now that we've created it.
+    my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id);
+    
+    unless ($val) {
+       $RT::Logger->err("$self ->Create couldn't set EffectiveId: $msg\n");
+    }  
+     
+
+    my $watcher;
+    foreach $watcher (@{$args{'Cc'}}) {
+       my ($wval, $wmsg) = 
+         $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1);
+       push @non_fatal_errors, $wmsg   unless ($wval);
+    }  
+
+    foreach $watcher (@{$args{'Requestor'}}) {
+       my ($wval, $wmsg) = 
+         $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1);
+       push @non_fatal_errors, $wmsg   unless ($wval);
+    }
+
+    foreach $watcher (@{$args{'AdminCc'}}) {
+       # Note that we're using AddWatcher, rather than _AddWatcher, as we 
+       # actually _want_ that ACL check. Otherwise, random ticket creators
+       # could make themselves adminccs and maybe get ticket rights. that would
+       # be poor
+       my ($wval, $wmsg) = 
+         $self->AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1);
+       push @non_fatal_errors, $wmsg   unless ($wval);
+    }
+
+    # Iterate through all the KeywordSelect-<int> params passed in, calling _AddKeyword
+    # for each of them
+
+
+    foreach my $key (keys %args) {
+
+       next unless ($key =~ /^KeywordSelect-(.*)$/);
+       
+       my $ks = $1;
+
+
+       my @keywords = ref($args{$key}) eq 'ARRAY' ?
+             @{$args{$key}} : ($args{$key});
+       
+       foreach my $keyword (@keywords) {  
+           my ($kval, $kmsg) = $self->_AddKeyword(KeywordSelect => $ks,
+                                                  Keyword => $keyword,
+                                                  Silent => 1);
+       }       
+       push @non_fatal_errors, $kmsg unless ($kval);
+    }
+
+    
+    
+    #Add a transaction for the create
+    my ($Trans, $Msg, $TransObj) = 
+       $self->_NewTransaction( Type => "Create",
+                               TimeTaken => 0, 
+                               MIMEObj=>$args{'MIMEObj'});
+    
+    # Logging
+    if ($self->Id && $Trans) {
+       $ErrStr = "Ticket ".$self->Id . " created in queue '". $QueueObj->Name. 
+         "'.\n" . join("\n", @non_fatal_errors);
+       
+       $RT::Logger->info($ErrStr);
+    }
+    else {
+       # TODO where does this get errstr from?
+       $RT::Logger->warning("Ticket couldn't be created: $ErrStr");
+    }
+    
+    return($self->Id, $TransObj->Id, $ErrStr);
+}
+
+# }}}
+
+# {{{ sub Import
+
+=head2 Import PARAMHASH
+
+Import a ticket. 
+Doesn\'t create a transaction. 
+Doesn\'t supply queue defaults, etc.
+
+Arguments are identical to Create(), with the addition of
+    Id -    Ticket Id
+
+Returns: TICKETID
+
+=cut
+
+
+sub Import {
+    my $self = shift;
+    my ( $ErrStr, $QueueObj, $Owner);
+    
+    my %args = (id => undef,
+               EffectiveId => undef,
+               Queue => undef,
+               Requestor => undef,
+               Type => 'ticket',
+               Owner => $RT::Nobody->Id,
+               Subject => '[no subject]',
+               InitialPriority => undef,
+               FinalPriority => undef,
+               Status => 'new',
+               TimeWorked => "0",
+               Due => undef,
+               Created => undef,
+               Updated => undef,
+       Resolved => undef,
+               Told => undef,
+               @_);
+    
+    if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) {
+       $QueueObj=RT::Queue->new($RT::SystemUser);
+       $QueueObj->Load($args{'Queue'});
+       #TODO error check this and return 0 if it\'s not loading properly +++
+    }
+    elsif (ref($args{'Queue'}) eq 'RT::Queue') {
+       $QueueObj=RT::Queue->new($RT::SystemUser);
+       $QueueObj->Load($args{'Queue'}->Id);
+    }
+    else {
+       $RT::Logger->debug("$self ". $args{'Queue'} . 
+                          " not a recognised queue object.");
+    }
+    
+    #Can't create a ticket without a queue.
+    unless (defined ($QueueObj) and $QueueObj->Id) {
+       $RT::Logger->debug( "$self No queue given for ticket creation.");
+       return (0,'Could not create ticket. Queue not set');
+    }
+    
+    #Now that we have a queue, Check the ACLS
+    unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket',
+                                             QueueObj => $QueueObj )) {
+       return (0,"No permission to create tickets in the queue '". 
+               $QueueObj->Name."'.");
+    }
+    
+    
+
+
+    # {{{ Deal with setting the owner
+      
+    # Attempt to take user object, user name or user id.
+    # Assign to nobody if lookup fails.
+    if (defined ($args{'Owner'})) { 
+       if ( ref($args{'Owner'}) ) {
+           $Owner = $args{'Owner'};
+       }
+       else {
+           $Owner = new RT::User($self->CurrentUser);
+           $Owner->Load($args{'Owner'});
+           if ( ! defined($Owner->id) ) {
+               $Owner->Load($RT::Nobody->id);
+           }
+       }
+    }
+    
+
+    #If we have a proposed owner and they don't have the right 
+    #to own a ticket, scream about it and make them not the owner
+    if ((defined ($Owner)) and
+       ($Owner->Id != $RT::Nobody->Id) and 
+       (!$Owner->HasQueueRight( QueueObj => $QueueObj, 
+                                Right => 'OwnTicket'))) {
+       
+       $RT::Logger->warning("$self user ".$Owner->Name . "(".$Owner->id .
+                            ") was proposed ".
+                            "as a ticket owner but has no rights to own ".
+                            "tickets in '".$QueueObj->Name."'\n");
+       
+       $Owner = undef;
+    }
+    
+    #If we haven't been handed a valid owner, make it nobody.
+    unless (defined ($Owner)) {
+       $Owner = new RT::User($self->CurrentUser);
+       $Owner->Load($RT::Nobody->UserObj->Id);
+    }  
+
+    # }}}
+
+    unless ($self->ValidateStatus($args{'Status'})) {
+       return (0,"'$args{'Status'}' is an invalid value for status");
+    }
+    
+    $self->{'_AccessibleCache'}{Created} = { 'read'=>1, 'write'=>1 };
+    $self->{'_AccessibleCache'}{Creator} = { 'read'=>1, 'auto'=>1 };
+    $self->{'_AccessibleCache'}{LastUpdated} = { 'read'=>1, 'write'=>1 };
+    $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read'=>1, 'auto'=>1 };
+
+
+    # If we're coming in with an id, set that now.
+    my $EffectiveId = undef;
+    if ($args{'id'}) {
+       $EffectiveId = $args{'id'};
+
+    }
+
+
+    my $id = $self->SUPER::Create(
+                                 id => $args{'id'},
+                                 EffectiveId => $EffectiveId,
+                                 Queue => $QueueObj->Id,
+                                 Owner => $Owner->Id,
+                                 Subject => $args{'Subject'},
+                                 InitialPriority => $args{'InitialPriority'},
+                                 FinalPriority => $args{'FinalPriority'},
+                                 Priority => $args{'InitialPriority'},
+                                 Status => $args{'Status'},
+                                 TimeWorked => $args{'TimeWorked'},
+                                 Type => $args{'Type'},        
+                                 Created => $args{'Created'},
+                                 Told => $args{'Told'},
+                                 LastUpdated => $args{'Updated'},
+       Resolved => $args{Resolved},
+                                 Due => $args{'Due'},
+                                );
+
+
+
+    # If the ticket didn't have an id
+    # Set the ticket's effective ID now that we've created it.
+    if ($args{'id'} ) { 
+         $self->Load($args{'id'});
+    }
+    else {
+          my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id);
+    
+          unless ($val) {
+           $RT::Logger->err($self."->Import couldn't set EffectiveId: $msg\n");
+          }    
+    } 
+
+    my $watcher;
+    foreach $watcher (@{$args{'Cc'}}) {
+       $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1);
+    }  
+    foreach $watcher (@{$args{'AdminCc'}}) {
+       $self->_AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1);
+    }  
+    foreach $watcher (@{$args{'Requestor'}}) {
+       $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1);
+    }
+    
+    return($self->Id, $ErrStr);
+}
+
+# }}}
+
+# {{{ sub Delete
+
+sub Delete {
+    my $self = shift;
+    return (0, 'Deleting this object would violate referential integrity.'.
+           ' That\'s bad.');
+}
+# }}}
+
+# {{{ Routines dealing with watchers.
+
+# {{{ Routines dealing with adding new watchers
+
+# {{{ sub AddWatcher
+
+=head2 AddWatcher
+
+AddWatcher takes a parameter hash. The keys are as follows:
+
+Email
+Type
+Owner
+
+If the watcher you\'re trying to set has an RT account, set the Owner paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
+
+=cut
+
+sub AddWatcher {
+    my $self = shift;
+    my %args = ( Email => undef,
+                Type => undef,
+                Owner => undef,
+                @_
+              );
+
+    # {{{ Check ACLS
+    #If the watcher we're trying to add is for the current user
+    if ( ( $self->CurrentUser->EmailAddress &&
+           ($args{'Email'} eq $self->CurrentUser->EmailAddress) ) or
+           ($args{'Owner'} eq $self->CurrentUser->Id) 
+        ) {
+
+       
+       #  If it's an AdminCc and they don't have 
+       #   'WatchAsAdminCc' or 'ModifyTicket', bail
+       if ($args{'Type'} eq 'AdminCc') {
+           unless ($self->CurrentUserHasRight('ModifyTicket') or 
+                   $self->CurrentUserHasRight('WatchAsAdminCc')) {
+               return(0, 'Permission Denied');
+           }
+       }
+
+       #  If it's a Requestor or Cc and they don't have
+       #   'Watch' or 'ModifyTicket', bail
+       elsif (($args{'Type'} eq 'Cc') or 
+              ($args{'Type'} eq 'Requestor')) {
+                  
+           unless ($self->CurrentUserHasRight('ModifyTicket') or 
+                   $self->CurrentUserHasRight('Watch')) {
+               return(0, 'Permission Denied');
+           }
+       }
+       else {
+           $RT::Logger->warn("$self -> AddWatcher hit code".
+                             " it never should. We got passed ".
+                             " a type of ". $args{'Type'});
+           return (0,'Error in parameters to TicketAddWatcher');
+       }
+    }
+    # If the watcher isn't the current user 
+    # and the current user  doesn't have 'ModifyTicket'
+    # bail
+    else {
+       unless ($self->CurrentUserHasRight('ModifyTicket')) {
+           return (0, "Permission Denied");
+       }
+    }
+    # }}}
+
+    return ($self->_AddWatcher(%args));
+}
+
+
+#This contains the meat of AddWatcher. but can be called from a routine like
+# Create, which doesn't need the additional acl check
+sub _AddWatcher {
+    my $self = shift;
+    my %args = (
+               Type => undef,
+               Silent => undef,
+               Email => undef,
+               Owner => 0,
+               Person => undef,
+               @_ );
+    
+    
+    
+    #clear the watchers cache
+    $self->{'watchers_cache'} = undef;
+    
+    if (defined $args{'Person'}) {
+       #if it's an RT::User object, pull out the id and shove it in Owner
+       if (ref ($args{'Person'}) =~ /RT::User/) {
+           $args{'Owner'} = $args{'Person'}->id;
+       }       
+       #if it's an int, shove it in Owner
+       elsif ($args{'Person'} =~ /^\d+$/) {
+           $args{'Owner'} = $args{'Person'};
+       }
+       #if it's an email address, shove it in Email
+       else {
+          $args{'Email'} = $args{'Person'};
+       }       
+    }  
+
+    # Turn an email address int a watcher if we possibly can.
+    if ($args{'Email'}) {
+       my $watcher = new RT::User($self->CurrentUser);
+       $watcher->LoadByEmail($args{'Email'});
+       if ($watcher->Id) {
+               $args{'Owner'} = $watcher->Id;
+               delete $args{'Email'};
+       }
+    }
+
+
+    # see if this user is already a watcher. if we have an owner, check it
+    # otherwise, we've got an email-address watcher. use that.
+
+    if ($self->IsWatcher(Type => $args{'Type'},
+                         Id => ($args{'Owner'} || $args{'Email'}) ) ) {
+
+
+        return(0, 'That user is already that sort of watcher for this ticket');
+    }
+
+    
+    require RT::Watcher;
+    my $Watcher = new RT::Watcher ($self->CurrentUser);
+    my ($retval, $msg) = ($Watcher->Create( Value => $self->Id,
+                                           Scope => 'Ticket',
+                                           Email => $args{'Email'},
+                                           Type => $args{'Type'},
+                                           Owner => $args{'Owner'},
+                                         ));
+    
+    unless ($args{'Silent'}) {
+       $self->_NewTransaction( Type => 'AddWatcher',
+                               NewValue => $Watcher->Email,
+                               Field => $Watcher->Type);
+    }
+    
+    return ($retval, $msg);
+}
+
+# }}}
+
+# {{{ sub AddRequestor
+
+=head2 AddRequestor
+
+AddRequestor takes what AddWatcher does, except it presets
+the "Type" parameter to \'Requestor\'
+
+=cut
+
+sub AddRequestor {
+   my $self = shift;
+   return ($self->AddWatcher ( Type => 'Requestor', @_));
+}
+
+# }}}
+
+# {{{ sub AddCc
+
+=head2 AddCc
+
+AddCc takes what AddWatcher does, except it presets
+the "Type" parameter to \'Cc\'
+
+=cut
+
+sub AddCc {
+   my $self = shift;
+   return ($self->AddWatcher ( Type => 'Cc', @_));
+}
+# }}}
+       
+# {{{ sub AddAdminCc
+
+=head2 AddAdminCc
+
+AddAdminCc takes what AddWatcher does, except it presets
+the "Type" parameter to \'AdminCc\'
+
+=cut
+
+sub AddAdminCc {
+   my $self = shift;
+   return ($self->AddWatcher ( Type => 'AdminCc', @_));
+}
+
+# }}}
+
+# }}}
+
+# {{{ sub DeleteWatcher
+
+=head2 DeleteWatcher id [type]
+
+DeleteWatcher takes a single argument which is either an email address 
+or a watcher id.  
+If the first argument is an email address, you need to specify the watcher type you're talking
+about as the second argument. Valid values are 'Requestor', 'Cc' or 'AdminCc'.
+It removes that watcher from this Ticket\'s list of watchers.
+
+
+=cut
+
+#TODO It is lame that you can't call this the same way you can call AddWatcher
+
+sub DeleteWatcher {
+    my $self = shift;
+    my $id = shift;
+
+    my $type;
+    
+    $type = shift if (@_);
+    
+    my $Watcher = new RT::Watcher($self->CurrentUser);
+    
+    #If it\'s a numeric watcherid
+    if ($id =~ /^(\d*)$/) {
+       $Watcher->Load($id);
+    }
+    
+    #Otherwise, we'll assume it's an email address
+    elsif ($type) {
+       my ($result, $msg) = 
+         $Watcher->LoadByValue( Email => $id,
+                                Scope => 'Ticket',
+                                Value => $self->id,
+                                Type => $type);
+       return (0,$msg) unless ($result);
+    }
+    
+    else {
+       return(0,"Can\'t delete a watcher by email address without specifying a type");
+    }
+    
+    # {{{ Check ACLS 
+
+    #If the watcher we're trying to delete is for the current user
+    if ($Watcher->Email eq $self->CurrentUser->EmailAddress) {
+               
+       #  If it's an AdminCc and they don't have 
+       #   'WatchAsAdminCc' or 'ModifyTicket', bail
+       if ($Watcher->Type eq 'AdminCc') {
+           unless ($self->CurrentUserHasRight('ModifyTicket') or 
+                   $self->CurrentUserHasRight('WatchAsAdminCc')) {
+               return(0, 'Permission Denied');
+           }
+       }
+
+       #  If it's a Requestor or Cc and they don't have
+       #   'Watch' or 'ModifyTicket', bail
+       elsif (($Watcher->Type eq 'Cc') or 
+              ($Watcher->Type eq 'Requestor')) {
+                  
+           unless ($self->CurrentUserHasRight('ModifyTicket') or 
+                   $self->CurrentUserHasRight('Watch')) {
+               return(0, 'Permission Denied');
+           }
+       }
+       else {
+           $RT::Logger->warn("$self -> DeleteWatcher hit code".
+                             " it never should. We got passed ".
+                             " a type of ". $args{'Type'});
+           return (0,'Error in parameters to $self DeleteWatcher');
+       }
+    }
+    # If the watcher isn't the current user 
+    # and the current user  doesn't have 'ModifyTicket'
+    # bail
+    else {
+       unless ($self->CurrentUserHasRight('ModifyTicket')) {
+           return (0, "Permission Denied");
+       }
+    }  
+    
+    # }}}
+    
+    unless (($Watcher->Scope eq 'Ticket') and
+           ($Watcher->Value == $self->id) ) {
+       return (0, "Not a watcher for this ticket");
+    }
+
+
+    #Clear out the watchers hash.
+    $self->{'watchers'} = undef;
+    
+    #If we\'ve validated that it is a watcher for this ticket 
+    $self->_NewTransaction ( Type => 'DelWatcher',        
+                            OldValue => $Watcher->Email,
+                            Field => $Watcher->Type,
+                          );
+    
+    my $retval = $Watcher->Delete();
+    
+    unless ($retval) {
+       return(0,"Watcher could not be deleted. Database inconsistency possible.");
+    }
+    
+    return(1, "Watcher deleted");
+}
+
+# {{{ sub DeleteRequestor
+
+=head2 DeleteRequestor EMAIL
+
+Takes an email address. It calls DeleteWatcher with a preset 
+type of 'Requestor'
+
+
+=cut
+
+sub DeleteRequestor {
+   my $self = shift;
+   my $id = shift;
+   return ($self->DeleteWatcher ($id, 'Requestor'))
+}
+
+# }}}
+
+# {{{ sub DeleteCc
+
+=head2 DeleteCc EMAIL
+
+Takes an email address. It calls DeleteWatcher with a preset 
+type of 'Cc'
+
+
+=cut
+
+sub DeleteCc {
+   my $self = shift;
+   my $id = shift;
+   return ($self->DeleteWatcher ($id, 'Cc'))
+}
+
+# }}}
+
+# {{{ sub DeleteAdminCc
+
+=head2 DeleteAdminCc EMAIL
+
+Takes an email address. It calls DeleteWatcher with a preset 
+type of 'AdminCc'
+
+
+=cut
+
+sub DeleteAdminCc {
+   my $self = shift;
+   my $id = shift;
+   return ($self->DeleteWatcher ($id, 'AdminCc'))
+}
+
+# }}}
+
+
+# }}}
+
+# {{{ sub Watchers
+
+=head2 Watchers
+
+Watchers returns a Watchers object preloaded with this ticket\'s watchers.
+
+# It should return only the ticket watchers. the actual FooAsString
+# methods capture the queue watchers too. I don't feel thrilled about this,
+# but we don't want the Cc Requestors and AdminCc objects to get filled up
+# with all the queue watchers too. we've got seperate objects for that.
+  # should we rename these as s/(.*)AsString/$1Addresses/ or somesuch?
+
+=cut
+
+sub Watchers {
+  my $self = shift;
+  
+  require RT::Watchers;
+  my $watchers=RT::Watchers->new($self->CurrentUser);
+  if ($self->CurrentUserHasRight('ShowTicket')) {
+      $watchers->LimitToTicket($self->id);
+  }
+  
+  return($watchers);
+  
+}
+
+# }}}
+
+# {{{ a set of  [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
+
+=head2 RequestorsAsString
+
+ B<Returns> String: All Ticket Requestor email addresses as a string.
+
+=cut
+
+sub RequestorsAsString {
+    my $self=shift;
+
+    unless ($self->CurrentUserHasRight('ShowTicket')) {
+        return undef;
+    }
+    
+    return ($self->Requestors->EmailsAsString() );
+}
+
+=head2 WatchersAsString
+
+B<Returns> String: All Ticket Watchers email addresses as a string
+
+=cut
+
+sub WatchersAsString {
+    my $self=shift;
+
+    unless ($self->CurrentUserHasRight('ShowTicket')) {
+       return (0, "Permission Denied");
+    }
+    
+    return ($self->Watchers->EmailsAsString());
+
+}
+
+=head2 AdminCcAsString
+
+returns String: All Ticket AdminCc email addresses as a string
+
+=cut
+
+
+sub AdminCcAsString {
+    my $self=shift;
+
+    unless ($self->CurrentUserHasRight('ShowTicket')) {
+       return undef;
+    }
+    
+    return ($self->AdminCc->EmailsAsString());
+    
+}
+
+=head2 CcAsString
+
+returns String: All Ticket Ccs as a string of email addresses
+
+=cut
+
+sub CcAsString {
+    my $self=shift;
+
+    unless ($self->CurrentUserHasRight('ShowTicket')) {
+        return undef; 
+    }
+    
+    return ($self->Cc->EmailsAsString());
+
+}
+
+# }}}
+
+# {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
+
+# {{{ sub Requestors
+
+=head2 Requestors
+
+Takes nothing.
+Returns this ticket's Requestors as an RT::Watchers object
+
+=cut
+
+sub Requestors {
+    my $self = shift;
+    
+    my $requestors = $self->Watchers();
+    if ($self->CurrentUserHasRight('ShowTicket')) {
+       $requestors->LimitToRequestors();
+    }  
+    
+    return($requestors);
+    
+}
+
+# }}}
+
+# {{{ sub Cc
+
+=head2 Cc
+
+Takes nothing.
+Returns a watchers object which contains this ticket's Cc watchers
+
+=cut
+
+sub Cc {
+    my $self = shift;
+    
+    my $cc = $self->Watchers();
+    
+    if ($self->CurrentUserHasRight('ShowTicket')) {
+       $cc->LimitToCc();
+    }
+    
+    return($cc);
+    
+}
+
+# }}}
+
+# {{{ sub AdminCc
+
+=head2 AdminCc
+
+Takes nothing.
+Returns this ticket\'s administrative Ccs as an RT::Watchers object
+
+=cut
+
+sub AdminCc {
+    my $self = shift;
+    
+    my $admincc = $self->Watchers();
+    if ($self->CurrentUserHasRight('ShowTicket')) {
+       $admincc->LimitToAdminCc();
+    }
+    return($admincc);
+}
+
+# }}}
+
+# }}}
+
+# {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
+
+# {{{ sub IsWatcher
+# a generic routine to be called by IsRequestor, IsCc and IsAdminCc
+
+=head2 IsWatcher
+
+Takes a param hash with the attributes Type and User. User is either a user object or string containing an email address. Returns true if that user or string
+is a ticket watcher. Returns undef otherwise
+
+=cut
+
+sub IsWatcher {
+    my $self = shift;
+
+    my %args = ( Type => 'Requestor',
+                Email => undef,
+                Id => undef,
+                @_
+              );
+    
+    my %cols = ('Type' => $args{'Type'},
+               'Scope' => 'Ticket',
+               'Value' => $self->Id,
+               'Owner' => undef,
+               'Email' => undef
+              );
+    
+    if (ref($args{'Id'})){ 
+       #If it's a ref, it's an RT::User object;
+       $cols{'Owner'} = $args{'Id'}->Id;
+    }
+    elsif ($args{'Id'} =~ /^\d+$/) { 
+       # if it's an integer, it's a reference to an RT::User obj
+       $cols{'Owner'} = $args{'Id'};
+    }
+    else {
+       $cols{'Email'} = $args{'Id'};
+    }  
+    
+    if ($args{'Email'}) {
+       $cols{'Email'} = $args{'Email'};
+    }
+
+    my $description = join(":",%cols);
+    
+    #If we've cached a positive match...
+    if (defined $self->{'watchers_cache'}->{"$description"}) {
+       if ($self->{'watchers_cache'}->{"$description"} == 1) {
+           return(1);
+       }
+       else { #If we've cached a negative match...
+           return(undef);
+       }
+    }
+    
+    
+    my $watcher = new RT::Watcher($self->CurrentUser);
+    $watcher->LoadByCols(%cols);
+    
+    
+    if ($watcher->id) {
+       $self->{'watchers_cache'}->{"$description"} = 1;
+       return(1);
+    }  
+    else {
+       $self->{'watchers_cache'}->{"$description"} = 0;
+       return(undef);
+    }
+    
+}
+# }}}
+
+# {{{ sub IsRequestor
+
+=head2 IsRequestor
+  
+  Takes an email address, RT::User object or integer (RT user id)
+  Returns true if the string is a requestor of the current ticket.
+
+
+=cut
+
+sub IsRequestor {
+    my $self = shift;
+    my $person = shift;
+
+    return ($self->IsWatcher(Type => 'Requestor', Id => $person));
+           
+};
+
+# }}}
+
+# {{{ sub IsCc
+
+=head2 IsCc
+
+Takes a string. Returns true if the string is a Cc watcher of the current ticket.
+
+=cut
+
+sub IsCc {
+  my $self = shift;
+  my $cc = shift;
+  
+  return ($self->IsWatcher( Type => 'Cc', Id => $cc ));
+  
+}
+
+# }}}
+
+# {{{ sub IsAdminCc
+
+=head2 IsAdminCc
+
+Takes a string. Returns true if the string is an AdminCc watcher of the current ticket.
+
+=cut
+
+sub IsAdminCc {
+  my $self = shift;
+  my $person = shift;
+  
+  return ($self->IsWatcher( Type => 'AdminCc', Id => $person ));
+  
+}
+
+# }}}
+
+# {{{ sub IsOwner
+
+=head2 IsOwner
+
+  Takes an RT::User object. Returns true if that user is this ticket's owner.
+returns undef otherwise
+
+=cut
+
+sub IsOwner {
+    my $self = shift;
+    my $person = shift;
+  
+
+    # no ACL check since this is used in acl decisions
+    # unless ($self->CurrentUserHasRight('ShowTicket')) {
+    #  return(undef);
+    #   }      
+
+    
+    #Tickets won't yet have owners when they're being created.
+    unless ($self->OwnerObj->id) {
+        return(undef);
+    }
+    
+    if ($person->id == $self->OwnerObj->id) {
+       return(1);
+    }
+    else {
+       return(undef);
+    }
+}
+
+
+# }}}
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with queues 
+
+# {{{ sub ValidateQueue
+
+sub ValidateQueue {
+  my $self = shift;
+  my $Value = shift;
+  
+  #TODO I don't think this should be here. We shouldn't allow anything to have an undef queue,
+  if (!$Value) {
+    $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
+    return (1);
+  }
+  
+  my $QueueObj = RT::Queue->new($self->CurrentUser);
+  my $id = $QueueObj->Load($Value);
+  
+  if ($id) {
+    return (1);
+  }
+  else {
+    return (undef);
+  }
+}
+
+# }}}
+
+# {{{ sub SetQueue  
+
+sub SetQueue {
+    my $self = shift;
+    my $NewQueue = shift;
+
+    #Redundant. ACL gets checked in _Set;
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {
+       return (0, "Permission Denied");
+    }
+   
+    my $NewQueueObj = RT::Queue->new($self->CurrentUser);
+    $NewQueueObj->Load($NewQueue);
+    
+    unless ($NewQueueObj->Id()) {
+       return (0, "That queue does not exist");
+    }
+    
+    if ($NewQueueObj->Id == $self->QueueObj->Id) {
+       return (0, 'That is the same value');
+    }
+    unless ($self->CurrentUser->HasQueueRight(Right =>'CreateTicket',
+                                             QueueObj => $NewQueueObj )) {
+       return (0, "You may not create requests in that queue.");
+    }
+    
+    unless ($self->OwnerObj->HasQueueRight(Right=> 'OwnTicket',  
+                                          QueueObj => $NewQueueObj)) {
+           $self->Untake();
+    }
+
+    return($self->_Set(Field => 'Queue', Value => $NewQueueObj->Id()));
+    
+}
+
+# }}}
+
+# {{{ sub QueueObj
+
+=head2 QueueObj
+
+Takes nothing. returns this ticket's queue object
+
+=cut
+
+sub QueueObj {
+    my $self = shift;
+    
+    my $queue_obj = RT::Queue->new($self->CurrentUser);
+    #We call __Value so that we can avoid the ACL decision and some deep recursion
+    my ($result) = $queue_obj->Load($self->__Value('Queue'));
+    return ($queue_obj);
+}
+
+
+# }}}
+
+# }}}
+
+# {{{ Date printing routines
+
+# {{{ sub DueObj
+
+=head2 DueObj
+
+  Returns an RT::Date object containing this ticket's due date
+
+=cut
+sub DueObj {
+    my $self = shift;
+    
+    my $time = new RT::Date($self->CurrentUser);
+
+    # -1 is RT::Date slang for never
+    if ($self->Due) {
+       $time->Set(Format => 'sql', Value => $self->Due );
+    }
+    else {
+       $time->Set(Format => 'unix', Value => -1);
+    }
+    
+    return $time;
+}
+# }}}
+
+# {{{ sub DueAsString 
+
+=head2 DueAsString
+
+Returns this ticket's due date as a human readable string
+
+=cut
+
+sub DueAsString {
+  my $self = shift;
+  return $self->DueObj->AsString();
+}
+
+# }}}
+
+# {{{ sub GraceTimeAsString 
+
+=head2 GraceTimeAsString
+
+Return the time until this ticket is due as a string
+
+=cut
+
+# TODO This should be deprecated 
+
+sub GraceTimeAsString {
+    my $self=shift;
+    
+    if ($self->Due) {
+       return ($self->DueObj->AgeAsString());
+    } else {
+       return "";
+    }
+}
+
+# }}}
+
+
+# {{{ sub ResolvedObj
+
+=head2 ResolvedObj
+
+  Returns an RT::Date object of this ticket's 'resolved' time.
+
+=cut
+
+sub ResolvedObj {
+  my $self = shift;
+
+  my $time = new RT::Date($self->CurrentUser);
+  $time->Set(Format => 'sql', Value => $self->Resolved);
+  return $time;
+}
+# }}}
+
+# {{{ sub SetStarted
+
+=head2 SetStarted
+
+Takes a date in ISO format or undef
+Returns a transaction id and a message
+The client calls "Start" to note that the project was started on the date in $date.
+A null date means "now"
+
+=cut
+  
+sub SetStarted {
+    my $self = shift;
+    my $time = shift || 0;
+    
+
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {
+       return (0, "Permission Denied");
+    }
+
+    #We create a date object to catch date weirdness
+    my $time_obj = new RT::Date($self->CurrentUser());
+    if ($time != 0)  {
+       $time_obj->Set(Format => 'ISO', Value => $time);
+    }
+    else {
+       $time_obj->SetToNow();
+    }
+    
+    #Now that we're starting, open this ticket
+    #TODO do we really want to force this as policy? it should be a scrip
+    
+    #We need $TicketAsSystem, in case the current user doesn't have
+    #ShowTicket
+    #
+    my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
+    $TicketAsSystem->Load($self->Id);  
+    if ($TicketAsSystem->Status eq 'new') {
+       $TicketAsSystem->Open();
+    }  
+    
+    return ($self->_Set(Field => 'Started', Value =>$time_obj->ISO));
+    
+}
+
+# }}}
+
+# {{{ sub StartedObj
+
+=head2 StartedObj
+
+  Returns an RT::Date object which contains this ticket's 
+'Started' time.
+
+=cut
+
+
+sub StartedObj {
+    my $self = shift;
+    
+    my $time = new RT::Date($self->CurrentUser);
+    $time->Set(Format => 'sql', Value => $self->Started);
+    return $time;
+}
+# }}}
+
+# {{{ sub StartsObj
+
+=head2 StartsObj
+
+  Returns an RT::Date object which contains this ticket's 
+'Starts' time.
+
+=cut
+
+sub StartsObj {
+  my $self = shift;
+  
+  my $time = new RT::Date($self->CurrentUser);
+  $time->Set(Format => 'sql', Value => $self->Starts);
+  return $time;
+}
+# }}}
+
+# {{{ sub ToldObj
+
+=head2 ToldObj
+
+  Returns an RT::Date object which contains this ticket's 
+'Told' time.
+
+=cut
+
+
+sub ToldObj {
+  my $self = shift;
+  
+  my $time = new RT::Date($self->CurrentUser);
+  $time->Set(Format => 'sql', Value => $self->Told);
+  return $time;
+}
+
+# }}}
+
+# {{{ sub LongSinceToldAsString
+
+# TODO this should be deprecated
+
+
+sub LongSinceToldAsString {
+  my $self = shift;
+
+  if ($self->Told) {
+      return $self->ToldObj->AgeAsString();
+  } else {
+      return "Never";
+  }
+}
+# }}}
+
+# {{{ sub ToldAsString
+
+=head2 ToldAsString
+
+A convenience method that returns ToldObj->AsString
+
+TODO: This should be deprecated
+
+=cut
+
+
+sub ToldAsString {
+    my $self = shift;
+    if ($self->Told) {
+       return $self->ToldObj->AsString();
+    }
+    else {
+       return("Never");
+    }
+}
+# }}}
+
+# {{{ sub TimeWorkedAsString
+
+=head2 TimeWorkedAsString
+
+Returns the amount of time worked on this ticket as a Text String
+
+=cut
+
+sub TimeWorkedAsString {
+    my $self=shift;
+    return "0" unless $self->TimeWorked;
+    
+    #This is not really a date object, but if we diff a number of seconds 
+    #vs the epoch, we'll get a nice description of time worked.
+    
+    my $worked = new RT::Date($self->CurrentUser);
+    #return the  #of minutes worked turned into seconds and written as
+    # a simple text string
+
+    return($worked->DurationAsString($self->TimeWorked*60));
+}
+
+# }}}
+
+
+# }}}
+
+# {{{ Routines dealing with correspondence/comments
+
+# {{{ sub Comment
+
+=head2 Comment
+
+Comment on this ticket.
+Takes a hashref with the follwoing attributes:
+
+MIMEObj, TimeTaken, CcMessageTo, BccMessageTo
+
+=cut
+
+sub Comment {
+  my $self = shift;
+  
+  my %args = (
+          CcMessageTo => undef,
+          BccMessageTo => undef,
+             MIMEObj => undef,
+             TimeTaken => 0,
+             @_ );
+
+  unless (($self->CurrentUserHasRight('CommentOnTicket')) or
+         ($self->CurrentUserHasRight('ModifyTicket'))) {
+      return (0, "Permission Denied");
+  }
+   unless ($args{'MIMEObj'}) {
+       return(0,"No correspondence attached");
+   }
+
+  # If we've been passed in CcMessageTo and BccMessageTo fields,
+  # add them to the mime object for passing on to the transaction handler
+  # The "NotifyOtherRecipients" scripAction will look for RT--Send-Cc: and
+  # RT-Send-Bcc: headers
+  $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'});
+  $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'});
+
+  #Record the correspondence (write the transaction)
+  my ($Trans, $Msg, $TransObj) = $self->_NewTransaction( Type => 'Comment',
+                                     Data =>($args{'MIMEObj'}->head->get('subject') || 'No Subject'),
+                                     TimeTaken => $args{'TimeTaken'},
+                                     MIMEObj => $args{'MIMEObj'}
+                                   );
+  
+  
+  return ($Trans, "The comment has been recorded");
+}
+
+# }}}
+
+# {{{ sub Correspond
+
+=head2 Correspond
+
+Correspond on this ticket.
+Takes a hashref with the following attributes:
+
+
+MIMEObj, TimeTaken, CcMessageTo, BccMessageTo
+
+=cut
+
+sub Correspond {
+    my $self = shift;
+    my %args = (
+          CcMessageTo => undef,
+          BccMessageTo => undef,
+               MIMEObj => undef,
+               TimeTaken => 0,
+               @_ );
+    
+    unless (($self->CurrentUserHasRight('ReplyToTicket')) or
+           ($self->CurrentUserHasRight('ModifyTicket'))) {
+       return (0, "Permission Denied");
+    }
+    
+    unless ($args{'MIMEObj'}) {
+       return(0,"No correspondence attached");
+    }
+    
+  # If we've been passed in CcMessageTo and BccMessageTo fields,
+  # add them to the mime object for passing on to the transaction handler
+  # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
+  # headers
+  $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'});
+  $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'});
+
+    #Record the correspondence (write the transaction)
+    my ($Trans,$msg, $TransObj) = $self->_NewTransaction
+      (Type => 'Correspond',
+       Data => ($args{'MIMEObj'}->head->get('subject') || 'No Subject'),
+       TimeTaken => $args{'TimeTaken'},
+       MIMEObj=> $args{'MIMEObj'}     
+      );
+    
+    # TODO this bit of logic should really become a scrip for 2.2
+    my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
+    $TicketAsSystem->Load($self->Id);
+       
+    if (
+       ($TicketAsSystem->Status ne 'open') and
+       ($TicketAsSystem->Status ne 'new')
+       ) {
+       
+       my $oldstatus = $TicketAsSystem->Status();
+       $TicketAsSystem->__Set(Field => 'Status', Value => 'open');
+       $TicketAsSystem->_NewTransaction 
+         ( Type => 'Set',
+           Field => 'Status',
+           OldValue => $oldstatus,
+           NewValue => 'open',
+           Data => 'Ticket auto-opened on incoming correspondence'
+         );
+    }
+    
+    unless ($Trans) {
+       $RT::Logger->err("$self couldn't init a transaction ($msg)\n");
+       return ($Trans, "correspondence (probably) not sent", $args{'MIMEObj'});
+    }
+    
+    #Set the last told date to now if this isn't mail from the requestor.
+    #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
+    
+    unless ($TransObj->IsInbound) {
+       $self->_SetTold;
+    }
+    
+    return ($Trans, "correspondence sent");
+}
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with Links and Relations between tickets
+
+# {{{ Link Collections
+
+# {{{ sub Members
+
+=head2 Members
+
+  This returns an RT::Links object which references all the tickets 
+which are 'MembersOf' this ticket
+
+=cut
+
+sub Members {
+   my $self = shift;
+   return ($self->_Links('Target', 'MemberOf'));
+}
+
+# }}}
+
+# {{{ sub MemberOf
+
+=head2 MemberOf
+
+  This returns an RT::Links object which references all the tickets that this
+ticket is a 'MemberOf'
+
+=cut
+
+sub MemberOf {
+   my $self = shift;
+   return ($self->_Links('Base', 'MemberOf'));
+}
+
+# }}}
+
+# {{{ RefersTo
+
+=head2 RefersTo
+
+  This returns an RT::Links object which shows all references for which this ticket is a base
+
+=cut
+
+sub RefersTo {
+    my $self = shift;
+    return ($self->_Links('Base', 'RefersTo'));
+}
+
+# }}}
+
+# {{{ ReferredToBy
+
+=head2 ReferredToBy
+
+  This returns an RT::Links object which shows all references for which this ticket is a target
+
+=cut
+
+sub ReferredToBy {
+    my $self = shift;
+    return ($self->_Links('Target', 'RefersTo'));
+}
+
+# }}}
+
+# {{{ DependedOnBy
+
+=head2 DependedOnBy
+
+  This returns an RT::Links object which references all the tickets that depend on this one
+
+=cut
+sub DependedOnBy {
+    my $self = shift;
+    return ($self->_Links('Target','DependsOn'));
+}
+
+# }}}
+
+# {{{ DependsOn
+
+=head2 DependsOn
+
+  This returns an RT::Links object which references all the tickets that this ticket depends on
+
+=cut
+sub DependsOn {
+   my $self = shift;
+    return ($self->_Links('Base','DependsOn'));
+}
+
+# }}}
+
+# {{{ sub _Links 
+
+sub _Links {
+    my $self = shift;
+    
+    #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
+    #tobias meant by $f
+    my $field = shift;
+    my $type =shift || "";
+
+    unless ($self->{"$field$type"}) {
+       $self->{"$field$type"} = new RT::Links($self->CurrentUser);
+       if ($self->CurrentUserHasRight('ShowTicket')) {
+           
+           $self->{"$field$type"}->Limit(FIELD=>$field, VALUE=>$self->URI);
+           $self->{"$field$type"}->Limit(FIELD=>'Type', 
+                                         VALUE=>$type) if ($type);
+       }
+    }
+    return ($self->{"$field$type"});
+}
+
+# }}}
+
+# }}}
+
+
+# {{{ sub DeleteLink 
+
+=head2 DeleteLink
+
+Delete a link. takes a paramhash of Base, Target and Type.
+Either Base or Target must be null. The null value will 
+be replaced with this ticket\'s id
+
+=cut 
+
+sub DeleteLink {
+    my $self = shift;
+    my %args = ( Base =>  undef,
+                Target => undef,
+                Type => undef,
+                @_ );
+    
+    #check acls
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {
+        $RT::Logger->debug("No permission to delete links\n"); 
+        return (0, 'Permission Denied');
+
+    
+    }
+    
+    #we want one of base and target. we don't care which
+    #but we only want _one_
+
+    if ($args{'Base'} and $args{'Target'}) {
+       $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
+       return (0, 'Can\'t specifiy both base and target');
+    }
+    elsif ($args{'Base'}) {
+       $args{'Target'} = $self->Id();
+    }
+    elsif ($args{'Target'}) {
+       $args{'Base'} = $self->Id();
+    }
+    else {  
+        $RT::Logger->debug("$self: Base or Target must be specified\n");
+       return (0, 'Either base or target must be specified');
+    }
+     
+    my $link = new RT::Link($self->CurrentUser);
+    $RT::Logger->debug("Trying to load link: ". $args{'Base'}." ". $args{'Type'}. " ". $args{'Target'}. "\n");
+    
+    $link->Load($args{'Base'}, $args{'Type'}, $args{'Target'});
+    
+    
+    
+    #it's a real link. 
+    if ($link->id) {
+        $RT::Logger->debug("We're going to delete link ".$link->id."\n");
+       $link->Delete();
+
+       my $TransString=
+         "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}.";
+       my ($Trans, $Msg, $TransObj) = $self->_NewTransaction
+         (Type => 'DeleteLink',
+          Field => $args{'Type'},
+          Data => $TransString,
+          TimeTaken => 0
+         );
+       
+       return ($linkid, "Link deleted ($TransString)", $transactionid);
+    }
+    #if it's not a link we can find
+    else {
+        $RT::Logger->debug("Couldn't find that link\n");
+       return (0, "Link not found");
+    }
+}
+
+# }}}
+
+# {{{ sub AddLink
+
+=head2 AddLink
+
+Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
+
+
+=cut
+
+sub AddLink {
+    my $self = shift;
+    my %args = ( Target => '',
+                Base => '',
+                Type => '',
+                @_ );
+    
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {
+       return (0, "Permission Denied");
+    }
+    
+    if ($args{'Base'} and $args{'Target'}) {
+       $RT::Logger->debug("$self tried to delete a link. both base and target were specified\n");
+       return (0, 'Can\'t specifiy both base and target');
+    }
+    elsif ($args{'Base'}) {
+        $args{'Target'} = $self->Id();
+    }
+    elsif ($args{'Target'}) {
+       $args{'Base'} = $self->Id();
+    }
+    else {  
+       return (0, 'Either base or target must be specified');
+    }
+    
+    # {{{ We don't want references to ourself
+    if ($args{Base} eq $args{Target}) {
+       return (0, "Can\'t link a ticket to itself");
+    }  
+               
+    # }}}
+    
+    # If the base isn't a URI, make it a URI. 
+    # If the target isn't a URI, make it a URI. 
+        
+    # {{{ Check if the link already exists - we don't want duplicates
+    my $old_link= new RT::Link ($self->CurrentUser);
+    $old_link->Load($args{'Base'}, $args{'Type'}, $args{'Target'});
+    if ($old_link->Id) {
+       $RT::Logger->debug("$self Somebody tried to duplicate a link");
+       return ($old_link->id, "Link already exists",0);
+    }
+    # }}}
+    
+    # Storing the link in the DB.
+    my $link = RT::Link->new($self->CurrentUser);
+    my ($linkid) = $link->Create(Target => $args{Target}, 
+                                Base => $args{Base}, 
+                                Type => $args{Type});
+    
+    unless ($linkid) {
+       return (0,"Link could not be created");
+    }
+       #Write the transaction
+    
+    my $TransString="Ticket $args{'Base'} $args{Type} ticket $args{'Target'}.";
+    
+    my ($Trans, $Msg, $TransObj) = $self->_NewTransaction
+      (Type => 'AddLink',
+       Field => $args{'Type'},
+       Data => $TransString,
+       TimeTaken => 0
+      );
+    
+    return ($Trans, "Link created ($TransString)");
+       
+       
+}
+# }}}
+
+# {{{ sub URI 
+
+=head2 URI
+
+Returns this ticket's URI
+
+=cut
+
+sub URI {
+    my $self = shift;
+    return $RT::TicketBaseURI.$self->id;
+}
+
+# }}}
+
+# {{{ sub MergeInto
+
+=head2 MergeInto
+MergeInto take the id of the ticket to merge this ticket into.
+
+=cut
+
+sub MergeInto {
+    my $self = shift;
+    my $MergeInto = shift;
+    
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {
+       return (0, "Permission Denied");
+    }
+    
+    # Load up the new ticket.
+    my $NewTicket = RT::Ticket->new($RT::SystemUser);
+    $NewTicket->Load($MergeInto);
+
+    # make sure it exists.
+    unless (defined $NewTicket->Id) {
+       return (0, 'New ticket doesn\'t exist');
+    }
+
+    
+    # Make sure the current user can modify the new ticket.
+    unless ($NewTicket->CurrentUserHasRight('ModifyTicket')) {
+       $RT::Logger->debug("failed...");
+       return (0, "Permission Denied");
+    }
+    
+    $RT::Logger->debug("checking if the new ticket has the same id and effective id...");
+    unless ($NewTicket->id == $NewTicket->EffectiveId) {
+       $RT::Logger->err('$self trying to merge into '.$NewTicket->Id .
+                        ' which is itself merged.\n');
+       return (0, "Can't merge into a merged ticket. ".
+               "You should never get this error");
+    }
+
+    
+    # We use EffectiveId here even though it duplicates information from
+    # the links table becasue of the massive performance hit we'd take
+    # by trying to do a seperate database query for merge info everytime 
+    # loaded a ticket. 
+    
+    
+    #update this ticket's effective id to the new ticket's id.
+    my ($id_val, $id_msg) = $self->__Set(Field => 'EffectiveId', 
+                                        Value => $NewTicket->Id());
+    
+    unless ($id_val) {
+       $RT::Logger->error("Couldn't set effective ID for ".$self->Id.
+                          ": $id_msg");
+       return(0,"Merge failed. Couldn't set EffectiveId");
+    }
+    
+    my ($status_val, $status_msg) = $self->__Set(Field => 'Status',
+                                                Value => 'resolved');
+    
+    unless ($status_val) {
+       $RT::Logger->error("$self couldn't set status to resolved.".
+                          "RT's Database may be inconsistent.");
+    }      
+    
+    #make a new link: this ticket is merged into that other ticket.
+    $self->AddLink( Type =>'MergedInto',
+                   Target => $NewTicket->Id() );
+    
+    #add all of this ticket's watchers to that ticket.
+    my $watchers = $self->Watchers();
+    
+    while (my $watcher = $watchers->Next()) {
+       unless (
+               ($watcher->Owner && 
+               $NewTicket->IsWatcher (Type => $watcher->Type,
+                                      Id => $watcher->Owner)) or 
+               ($watcher->Email && 
+                $NewTicket->IsWatcher (Type => $watcher->Type,
+                                       Id => $watcher->Email)) 
+              ) {
+           
+           
+           
+           $NewTicket->_AddWatcher(Silent => 1, 
+                                   Type => $watcher->Type, 
+                                   Email => $watcher->Email,
+                                   Owner => $watcher->Owner);
+       }
+    }
+    
+    
+    #find all of the tickets that were merged into this ticket. 
+    my $old_mergees = new RT::Tickets($self->CurrentUser);
+    $old_mergees->Limit( FIELD => 'EffectiveId',
+                        OPERATOR => '=',
+                        VALUE => $self->Id );
+    
+    #   update their EffectiveId fields to the new ticket's id
+    while (my $ticket = $old_mergees->Next()) {
+       my ($val, $msg) = $ticket->__Set(Field => 'EffectiveId', 
+                                        Value => $NewTicket->Id());
+    }  
+    $NewTicket->_SetLastUpdated;
+
+    return ($TransactionObj, "Merge Successful");
+}  
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with keywords
+
+# {{{ sub KeywordsObj
+
+=head2 KeywordsObj [KEYWORD_SELECT_ID]
+
+  Returns an B<RT::ObjectKeywords> object preloaded with this ticket's ObjectKeywords.
+If the optional KEYWORD_SELECT_ID parameter is set, limit the keywords object to that keyword
+select.
+
+=cut
+
+sub KeywordsObj {
+    my $self = shift;
+    my $keyword_select; 
+    
+    $keyword_select = shift if (@_);
+    
+    use RT::ObjectKeywords;
+    my $Keywords = new RT::ObjectKeywords($self->CurrentUser);
+
+    #ACL check
+    if ($self->CurrentUserHasRight('ShowTicket')) {
+       $Keywords->LimitToTicket($self->id);
+       if ($keyword_select) {
+           $Keywords->LimitToKeywordSelect($keyword_select);
+       }       
+    }
+    return ($Keywords);
+}
+# }}}
+
+# {{{ sub AddKeyword
+
+=head2 AddKeyword
+
+Takes a paramhash of Keyword and KeywordSelect.  If Keyword is a valid choice
+for KeywordSelect, creates a KeywordObject.  If the KeywordSelect says this should
+be a single KeywordObject, automatically removes the old value.
+
+ Issues: probably doesn't enforce the depth restrictions or make sure that keywords
+are coming from the right part of the tree. really should.
+
+=cut
+
+sub AddKeyword {
+    my $self = shift;
+   #ACL check
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {
+       return (0, 'Permission Denied');
+    }
+    
+    return($self->_AddKeyword(@_));
+    
+}
+
+
+# Helper version of AddKeyword without that pesky ACL check
+sub _AddKeyword {
+    my $self = shift;
+    my %args = ( KeywordSelect => undef,  # id of a keyword select record
+                Keyword => undef, #id of the keyword to add
+                Silent => 0,
+                @_
+              );
+    
+    my ($OldValue);
+
+    #TODO make sure that $args{'Keyword'} is valid for $args{'KeywordSelect'}
+
+    #TODO: make sure that $args{'KeywordSelect'} applies to this ticket's queue.
+    
+    my $Keyword = new RT::Keyword($self->CurrentUser);
+    unless ($Keyword->Load($args{'Keyword'}) ) {
+       $RT::Logger->err("$self Couldn't load Keyword ".$args{'Keyword'} ."\n");
+       return(0, "Couldn't load keyword");
+    }
+    
+    my $KeywordSelectObj = new RT::KeywordSelect($self->CurrentUser);
+    unless ($KeywordSelectObj->Load($args{'KeywordSelect'})) {
+       $RT::Logger->err("$self Couldn't load KeywordSelect ".$args{'KeywordSelect'});
+       return(0, "Couldn't load keywordselect");
+    }
+    
+    my $Keywords = $self->KeywordsObj($KeywordSelectObj->id);
+
+    #If the ticket already has this keyword, just get out of here.
+    if ($Keywords->HasEntry($Keyword->id)) {
+       return(0, "That is already the current value");
+    }  
+
+    #If the keywordselect wants this to be a singleton:
+
+    if ($KeywordSelectObj->Single) {
+
+       #Whack any old values...keep track of the last value that we get.
+       #we shouldn't need a loop ehre, but we do it anyway, to try to 
+       # help keep the database clean.
+       while (my $OldKey = $Keywords->Next) {
+           $OldValue = $OldKey->KeywordObj->Name;
+           $OldKey->Delete();
+       }       
+       
+       
+    }
+
+    # create the new objectkeyword 
+    my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser);
+    my $result = $ObjectKeyword->Create( Keyword => $Keyword->Id,
+                                        ObjectType => 'Ticket',
+                                        ObjectId => $self->Id,
+                                        KeywordSelect => $KeywordSelectObj->Id );
+    
+
+    # record a single transaction, unless we were told not to
+    unless ($args{'Silent'}) {
+       my ($TransactionId, $Msg, $TransactionObj) = 
+         $self->_NewTransaction( Type => 'Keyword',
+                                 Field => $KeywordSelectObj->Id,
+                                 OldValue => $OldValue,
+                                 NewValue => $Keyword->Name );
+    }
+    return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." added.");    
+
+}      
+
+# }}}
+
+# {{{ sub DeleteKeyword
+
+=head2 DeleteKeyword
+  
+  Takes a paramhash. Deletes the Keyword denoted by the I<Keyword> parameter from this
+  ticket's object keywords.
+
+=cut
+
+sub DeleteKeyword {
+    my $self = shift;
+    my %args = ( Keyword => undef,
+                KeywordSelect => undef,
+                @_ );
+
+   #ACL check
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {    
+       return (0, 'Permission Denied');
+    }
+
+    
+    #Load up the ObjectKeyword we\'re talking about
+    my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser);
+    $ObjectKeyword->LoadByCols(Keyword => $args{'Keyword'},
+                              KeywordSelect => $args{'KeywordSelect'},
+                              ObjectType => 'Ticket',
+                              ObjectId => $self->id()
+                             );
+    
+    #if we can\'t find it, bail
+    unless ($ObjectKeyword->id) {
+       $RT::Logger->err("Couldn't find the keyword ".$args{'Keyword'} .
+                        " for keywordselect ". $args{'KeywordSelect'} . 
+                        "for ticket ".$self->id );
+       return (undef, "Couldn't load keyword while trying to delete it.");
+    };
+    
+    #record transaction here.
+    my ($TransactionId, $Msg, $TransObj) = 
+      $self->_NewTransaction( Type => 'Keyword', 
+                             OldValue => $ObjectKeyword->KeywordObj->Name);
+    
+    $ObjectKeyword->Delete();
+    
+    return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." deleted.");
+    
+}
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with ownership
+
+# {{{ sub OwnerObj
+
+=head2 OwnerObj
+
+Takes nothing and returns an RT::User object of 
+this ticket's owner
+
+=cut
+
+sub OwnerObj {
+    my $self = shift;
+    
+    #If this gets ACLed, we lose on a rights check in User.pm and
+    #get deep recursion. if we need ACLs here, we need
+    #an equiv without ACLs
+    
+    $owner = new RT::User ($self->CurrentUser);
+    $owner->Load($self->__Value('Owner'));
+    
+    #Return the owner object
+    return ($owner);
+}
+
+# }}}
+
+# {{{ sub OwnerAsString 
+
+=head2 OwnerAsString
+
+Returns the owner's email address
+
+=cut
+
+sub OwnerAsString {
+  my $self = shift;
+  return($self->OwnerObj->EmailAddress);
+
+}
+
+# }}}
+
+# {{{ sub SetOwner
+
+=head2 SetOwner
+
+Takes two arguments:
+     the Id or Name of the owner 
+and  (optionally) the type of the SetOwner Transaction. It defaults
+to 'Give'.  'Steal' is also a valid option.
+
+=cut
+
+sub SetOwner {
+    my $self = shift;
+    my $NewOwner = shift;
+    my $Type = shift || "Give";
+    
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {
+       return (0, "Permission Denied");
+    }  
+    
+    my $NewOwnerObj = RT::User->new($self->CurrentUser);
+    my $OldOwnerObj = $self->OwnerObj;
+  
+    $NewOwnerObj->Load($NewOwner);
+    if (!$NewOwnerObj->Id) {
+           return (0, "That user does not exist");
+    }
+    
+    #If thie ticket has an owner and it's not the current user
+    
+    if (($Type ne 'Steal' ) and ($Type ne 'Force') and #If we're not stealing
+       ($self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
+       ($self->CurrentUser->Id ne $self->OwnerObj->Id())) { #and it's not us
+       return(0, "You can only reassign tickets that you own or that are unowned");
+    }
+    
+    #If we've specified a new owner and that user can't modify the ticket
+    elsif (($NewOwnerObj->Id) and 
+          (!$NewOwnerObj->HasQueueRight(Right => 'OwnTicket',
+                                        QueueObj => $self->QueueObj,
+                                        TicketObj => $self))
+         ) {
+       return (0, "That user may not own requests in that queue");
+    }
+  
+  
+    #If the ticket has an owner and it's the new owner, we don't need
+    #To do anything
+    elsif (($self->OwnerObj) and ($NewOwnerObj->Id eq $self->OwnerObj->Id)) {
+       return(0, "That user already owns that request");
+    }
+  
+  
+    my ($trans,$msg)=$self->_Set(Field => 'Owner',
+                                Value => $NewOwnerObj->Id, 
+                                TimeTaken => 0,
+                                TransactionType => $Type);
+  
+    if ($trans) {
+       $msg = "Owner changed from ".$OldOwnerObj->Name." to ".$NewOwnerObj->Name;
+    }
+    return ($trans, $msg);
+         
+}
+
+# }}}
+
+# {{{ sub Take
+
+=head2 Take
+
+A convenince method to set the ticket's owner to the current user
+
+=cut
+
+sub Take {
+    my $self = shift;
+    return ($self->SetOwner($self->CurrentUser->Id, 'Take'));
+}
+
+# }}}
+
+# {{{ sub Untake
+
+=head2 Untake
+
+Convenience method to set the owner to 'nobody' if the current user is the owner.
+
+=cut
+
+sub Untake {
+    my $self = shift;
+    return($self->SetOwner($RT::Nobody->UserObj->Id, 'Untake'));
+}
+# }}}
+
+# {{{ sub Steal 
+
+=head2 Steal
+
+A convenience method to change the owner of the current ticket to the
+current user. Even if it's owned by another user.
+
+=cut
+
+sub Steal {
+    my $self = shift;
+  
+    if ($self->IsOwner($self->CurrentUser)) {
+       return (0,"You already own this ticket"); 
+    } else {
+       return($self->SetOwner($self->CurrentUser->Id, 'Steal'));
+      
+    }
+  
+}
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with status
+
+# {{{ sub ValidateStatus 
+
+=head2 ValidateStatus STATUS
+
+Takes a string. Returns true if that status is a valid status for this ticket.
+Returns false otherwise.
+
+=cut
+
+sub ValidateStatus {
+    my $self = shift;
+    my $status = shift;
+
+    #Make sure the status passed in is valid
+    unless ($self->QueueObj->IsValidStatus($status)) {
+       return (undef);
+    }
+    
+    return (1);
+
+}
+
+
+# }}}
+
+# {{{ sub SetStatus
+
+=head2 SetStatus STATUS
+
+Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved or dead.
+
+=cut
+
+sub SetStatus { 
+    my $self = shift;
+    my $status = shift;
+
+    #Check ACL
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {
+       return (0, 'Permission Denied');
+    }
+
+    my $now = new RT::Date($self->CurrentUser);
+    $now->SetToNow();
+    
+    #If we're changing the status from new, record that we've started
+    if (($self->Status =~ /new/) && ($status ne 'new')) {
+       #Set the Started time to "now"
+       $self->_Set(Field => 'Started',
+                   Value => $now->ISO,
+                   RecordTransaction => 0);
+    }
+    
+
+    if ($status eq 'resolved') {
+       #When we resolve a ticket, set the 'Resolved' attribute to now.
+       $self->_Set(Field => 'Resolved',
+                   Value => $now->ISO, 
+                   RecordTransaction => 0);
+    }
+    
+    
+    #Actually update the status
+    return($self->_Set(Field => 'Status', 
+                      Value => $status,
+                      TimeTaken => 0,
+                      TransactionType => 'Status'));
+}
+
+# }}}
+
+# {{{ sub Kill
+
+=head2 Kill
+
+Takes no arguments. Marks this ticket for garbage collection
+
+=cut
+
+sub Kill {
+  my $self = shift;
+  return ($self->SetStatus('dead'));
+  # TODO: garbage collection
+}
+
+# }}}
+
+# {{{ sub Stall
+
+=head2 Stall
+
+Sets this ticket's status to stalled
+
+=cut
+
+sub Stall {
+  my $self = shift;
+  return ($self->SetStatus('stalled'));
+}
+
+# }}}
+
+# {{{ sub Open
+
+=head2 Open
+
+Sets this ticket\'s status to Open
+
+=cut
+
+sub Open {
+    my $self = shift;
+    return ($self->SetStatus('open'));
+}
+
+# }}}
+
+# {{{ sub Resolve
+
+=head2 Resolve
+
+Sets this ticket\'s status to Resolved
+
+=cut
+
+sub Resolve {
+    my $self = shift;
+    return ($self->SetStatus('resolved'));
+}
+
+# }}}
+
+# }}}
+
+# {{{ Actions + Routines dealing with transactions
+
+# {{{ sub SetTold and _SetTold
+
+=head2 SetTold ISO  [TIMETAKEN]
+
+Updates the told and records a transaction
+
+=cut
+
+sub SetTold {
+    my $self=shift;
+    my $told;
+    $told = shift if (@_);
+    my $timetaken=shift || 0;
+   
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {
+       return (0, "Permission Denied");
+    }
+
+    my $datetold = new RT::Date($self->CurrentUser);
+    if ($told) {
+       $datetold->Set( Format => 'iso',
+                       Value => $told);
+    }
+    else {
+        $datetold->SetToNow(); 
+    }
+    
+    return($self->_Set(Field => 'Told', 
+                      Value => $datetold->ISO,
+                      TimeTaken => $timetaken,
+                      TransactionType => 'Told'));
+}
+
+=head2 _SetTold
+
+Updates the told without a transaction or acl check. Useful when we're sending replies.
+
+=cut
+
+sub _SetTold {
+    my $self=shift;
+    
+    my $now = new RT::Date($self->CurrentUser);
+    $now->SetToNow();
+    #use __Set to get no ACLs ;)
+    return($self->__Set(Field => 'Told',
+                       Value => $now->ISO));
+}
+
+# }}}
+
+# {{{ sub Transactions 
+
+=head2 Transactions
+
+  Returns an RT::Transactions object of all transactions on this ticket
+
+=cut
+  
+sub Transactions {
+    my $self = shift;
+    
+    use RT::Transactions;
+    my $transactions = RT::Transactions->new($self->CurrentUser);
+
+    #If the user has no rights, return an empty object
+    if ($self->CurrentUserHasRight('ShowTicket')) {
+       my $tickets = $transactions->NewAlias('Tickets');
+       $transactions->Join( ALIAS1 => 'main',
+                             FIELD1 => 'Ticket',
+                             ALIAS2 => $tickets,
+                             FIELD2 => 'id');
+       $transactions->Limit( ALIAS => $tickets,
+                             FIELD => 'EffectiveId',
+                             VALUE => $self->id());
+        # if the user may not see comments do not return them
+        unless ($self->CurrentUserHasRight('ShowTicketComments')) {
+            $transactions->Limit( FIELD => 'Type',
+                                  OPERATOR => '!=',
+                                  VALUE => "Comment");
+        }
+    }
+    
+    return($transactions);
+}
+
+# }}}
+
+# {{{ sub _NewTransaction
+
+sub _NewTransaction {
+    my $self = shift;
+    my %args = ( TimeTaken => 0,
+                Type => undef,
+                OldValue => undef,
+                NewValue => undef,
+                Data => undef,
+                Field => undef,
+                MIMEObj => undef,
+                @_ );
+    
+    
+    require RT::Transaction;
+    my $trans = new RT::Transaction($self->CurrentUser);
+    my ($transaction, $msg) = 
+      $trans->Create( Ticket => $self->Id,
+                     TimeTaken => $args{'TimeTaken'},
+                     Type => $args{'Type'},
+                     Data => $args{'Data'},
+                     Field => $args{'Field'},
+                     NewValue => $args{'NewValue'},
+                     OldValue => $args{'OldValue'},
+                     MIMEObj => $args{'MIMEObj'}
+                   );
+    
+    $RT::Logger->warning($msg) unless $transaction;
+    
+    $self->_SetLastUpdated;
+    
+    if (defined $args{'TimeTaken'} ) {
+       $self->_UpdateTimeTaken($args{'TimeTaken'}); 
+    }
+    return($transaction, $msg, $trans);
+}
+
+# }}}
+
+# }}}
+
+# {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
+
+# {{{ sub _ClassAccessible
+
+sub _ClassAccessible {
+    {
+       EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
+       Queue => { 'read' => 1, 'write' => 1 },
+       Requestors => { 'read' => 1, 'write' => 1 },
+       Owner => { 'read' => 1, 'write' => 1 },
+       Subject => { 'read' => 1, 'write' => 1 },
+       InitialPriority => { 'read' => 1, 'write' => 1 },
+       FinalPriority => { 'read' => 1, 'write' => 1 },
+       Priority => { 'read' => 1, 'write' => 1 },
+       Status => { 'read' => 1, 'write' => 1 },
+       TimeWorked => { 'read' => 1, 'write' => 1 },
+       TimeLeft => { 'read' => 1, 'write' => 1 },
+       Created => { 'read' => 1, 'auto' => 1 },
+       Creator => { 'read' => 1,  'auto' => 1 },
+       Told => { 'read' => 1, 'write' => 1 },
+       Resolved => {'read' => 1},
+       Starts => { 'read' => 1, 'write' => 1 },
+       Started => { 'read' => 1, 'write' => 1 },
+       Due => { 'read' => 1, 'write' => 1 },
+       Creator => { 'read' => 1, 'auto' => 1 },
+       Created => { 'read' => 1, 'auto' => 1 },
+       LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
+       LastUpdated => { 'read' => 1, 'auto' => 1 }
+    };
+
+}    
+
+# }}}
+
+# {{{ sub _Set
+
+sub _Set {
+    my $self = shift;
+    
+    unless ($self->CurrentUserHasRight('ModifyTicket')) {
+       return (0, "Permission Denied");
+    }
+    
+    my %args = (Field => undef,
+               Value => undef,
+               TimeTaken => 0,
+               RecordTransaction => 1,
+               TransactionType => 'Set',
+               @_
+              );
+    #if the user is trying to modify the record
+    
+    #Take care of the old value we really don't want to get in an ACL loop.
+    # so ask the super::_Value
+    my $Old=$self->SUPER::_Value("$args{'Field'}");
+    
+    #Set the new value
+    my ($ret, $msg)=$self->SUPER::_Set(Field => $args{'Field'}, 
+                                      Value=> $args{'Value'});
+    
+    #If we can't actually set the field to the value, don't record
+    # a transaction. instead, get out of here.
+    if ($ret==0) {return (0,$msg);}
+    
+    if ($args{'RecordTransaction'} == 1) {
+       
+       my ($Trans, $Msg, $TransObj) =  
+         $self->_NewTransaction(Type => $args{'TransactionType'},
+                                Field => $args{'Field'},
+                                NewValue => $args{'Value'},
+                                OldValue =>  $Old,
+                                TimeTaken => $args{'TimeTaken'},
+                               );
+      return ($Trans,$TransObj->Description);
+    }
+    else {
+       return ($ret, $msg);
+  }
+}
+
+# }}}
+
+# {{{ sub _Value 
+
+=head2 _Value
+
+Takes the name of a table column.
+Returns its value as a string, if the user passes an ACL check
+
+=cut
+
+sub _Value  {
+
+  my $self = shift;
+  my $field = shift;
+
+  
+  #if the field is public, return it.
+  if ($self->_Accessible($field, 'public')) {
+      #$RT::Logger->debug("Skipping ACL check for $field\n");
+      return($self->SUPER::_Value($field));
+      
+  }
+  
+  #If the current user doesn't have ACLs, don't let em at it.  
+  
+  unless ($self->CurrentUserHasRight('ShowTicket')) {
+      return (undef);
+  }
+  return($self->SUPER::_Value($field));
+  
+}
+
+# }}}
+
+# {{{ sub _UpdateTimeTaken
+
+=head2 _UpdateTimeTaken
+
+This routine will increment the timeworked counter. it should
+only be called from _NewTransaction 
+
+=cut
+
+sub _UpdateTimeTaken {
+  my $self = shift;
+  my $Minutes = shift;
+  my ($Total);
+   
+  $Total = $self->SUPER::_Value("TimeWorked");
+  $Total = ($Total || 0) + ($Minutes || 0);
+  $self->SUPER::_Set(Field => "TimeWorked", 
+                    Value => $Total);
+
+  return ($Total);
+}
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with ACCESS CONTROL
+
+# {{{ sub CurrentUserHasRight 
+
+=head2 CurrentUserHasRight
+
+  Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
+1 if the user has that right. It returns 0 if the user doesn't have that right.
+
+=cut
+
+sub CurrentUserHasRight {
+  my $self = shift;
+  my $right = shift;
+  
+  return ($self->HasRight( Principal=> $self->CurrentUser->UserObj(),
+                           Right => "$right"));
+
+}
+
+# }}}
+
+# {{{ sub HasRight 
+
+=head2 HasRight
+
+ Takes a paramhash with the attributes 'Right' and 'Principal'
+  'Right' is a ticket-scoped textual right from RT::ACE 
+  'Principal' is an RT::User object
+
+  Returns 1 if the principal has the right. Returns undef if not.
+
+=cut
+
+sub HasRight {
+    my $self = shift;
+    my %args = ( Right => undef,
+                Principal => undef,
+                @_);
+    
+    unless ((defined $args{'Principal'}) and (ref($args{'Principal'}))) {
+       $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight");
+    }
+    
+    return($args{'Principal'}->HasQueueRight(TicketObj => $self,
+                                            Right => $args{'Right'}));
+}
+
+# }}}
+
+# }}}
+
+
+1;
+
+=head1 AUTHOR
+
+Jesse Vincent, jesse@fsck.com
+
+=head1 SEE ALSO
+
+RT
+
+=cut
+
+
diff --git a/rt/lib/RT/Tickets.pm b/rt/lib/RT/Tickets.pm
new file mode 100755 (executable)
index 0000000..dd91126
--- /dev/null
@@ -0,0 +1,1789 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Tickets.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Tickets - A collection of Ticket objects
+
+
+=head1 SYNOPSIS
+
+  use RT::Tickets;
+  my $tickets = new RT::Tickets($CurrentUser);
+
+=head1 DESCRIPTION
+
+   A collection of RT::Tickets.
+
+=head1 METHODS
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Tickets);
+
+=end testing
+
+=cut
+
+package RT::Tickets;
+use RT::EasySearch;
+use RT::Ticket;
+@ISA= qw(RT::EasySearch);
+
+use vars qw(%TYPES @SORTFIELDS);
+
+# {{{ TYPES
+
+%TYPES =    ( Status => 'ENUM',
+             Queue  => 'ENUM',
+             Type => 'ENUM',
+             Creator => 'ENUM',
+             LastUpdatedBy => 'ENUM',
+             Owner => 'ENUM',
+             EffectiveId => 'INT',
+             id => 'INT',
+             InitialPriority => 'INT',
+             FinalPriority => 'INT',
+             Priority => 'INT',
+             TimeLeft => 'INT',
+             TimeWorked => 'INT',
+             MemberOf => 'LINK',
+             DependsOn => 'LINK',
+             HasMember => 'LINK',
+             HasDepender => 'LINK',
+             RelatedTo => 'LINK',
+              Told => 'DATE',
+              StartsBy => 'DATE',
+              Started => 'DATE',
+              Due  => 'DATE',
+              Resolved => 'DATE',
+              LastUpdated => 'DATE',
+              Created => 'DATE',
+              Subject => 'STRING',
+             Type => 'STRING',
+              Content => 'TRANSFIELD',
+             ContentType => 'TRANSFIELD',
+             TransactionDate => 'TRANSDATE',
+             Watcher => 'WATCHERFIELD',
+             LinkedTo => 'LINKFIELD',
+              Keyword => 'KEYWORDFIELD'
+
+           );
+
+
+# }}}
+
+# {{{ sub SortFields
+
+@SORTFIELDS = qw(id Status Owner Created Due Starts Started
+                Queue Subject Told Started 
+                   Resolved LastUpdated Priority TimeWorked TimeLeft);
+
+=head2 SortFields
+
+Returns the list of fields that lists of tickets can easily be sorted by
+
+=cut
+
+
+sub SortFields {
+       my $self = shift;
+       return(@SORTFIELDS);
+}
+
+
+# }}}
+
+# {{{ Limit the result set based on content
+
+# {{{ sub Limit 
+
+=head2 Limit
+
+Takes a paramhash with the fields FIELD, OPERATOR, VALUE and DESCRIPTION
+Generally best called from LimitFoo methods
+
+=cut
+sub Limit {
+    my $self = shift;
+    my %args = ( FIELD => undef,
+                OPERATOR => '=',
+                VALUE => undef,
+                DESCRIPTION => undef,
+                @_
+              );
+   $args{'DESCRIPTION'} = "Autodescribed: ".$args{'FIELD'} . $args{'OPERATOR'} . $args{'VALUE'},
+    if (!defined $args{'DESCRIPTION'}) ;
+
+    my $index = $self->_NextIndex;
+    
+    #make the TicketRestrictions hash the equivalent of whatever we just passed in;
+    
+    %{$self->{'TicketRestrictions'}{$index}} = %args;
+
+    $self->{'RecalcTicketLimits'} = 1;
+
+    # If we're looking at the effective id, we don't want to append the other clause
+    # which limits us to tickets where id = effective id 
+    if ($args{'FIELD'} eq 'EffectiveId') {
+        $self->{'looking_at_effective_id'} = 1;
+    }
+
+    return ($index);
+}
+
+# }}}
+
+
+
+
+=head2 FreezeLimits
+
+Returns a frozen string suitable for handing back to ThawLimits.
+
+=cut
+# {{{ sub FreezeLimits
+
+sub FreezeLimits {
+       my $self = shift;
+       require FreezeThaw;
+       return (FreezeThaw::freeze($self->{'TicketRestrictions'},
+                                  $self->{'restriction_index'}
+                                 ));
+}
+
+# }}}
+
+=head2 ThawLimits
+
+Take a frozen Limits string generated by FreezeLimits and make this tickets
+object have that set of limits.
+
+=cut
+# {{{ sub ThawLimits
+
+sub ThawLimits {
+       my $self = shift;
+       my $in = shift;
+       
+       #if we don't have $in, get outta here.
+       return undef unless ($in);
+
+       $self->{'RecalcTicketLimits'} = 1;
+
+       require FreezeThaw;
+       
+       #We don't need to die if the thaw fails.
+       
+       eval {
+               ($self->{'TicketRestrictions'},
+               $self->{'restriction_index'}
+               ) = FreezeThaw::thaw($in);
+       }
+
+}
+
+# }}}
+
+# {{{ Limit by enum or foreign key
+
+# {{{ sub LimitQueue
+
+=head2 LimitQueue
+
+LimitQueue takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of = or !=. (It defaults to =).
+VALUE is a queue id. 
+
+=cut
+
+sub LimitQueue {
+    my $self = shift;
+    my %args = (VALUE => undef,
+               OPERATOR => '=',
+               @_);
+
+    #TODO  VALUE should also take queue names and queue objects
+    my $queue = new RT::Queue($self->CurrentUser);
+    $queue->Load($args{'VALUE'});
+    
+    #TODO check for a valid queue here
+
+    $self->Limit (FIELD => 'Queue',
+                 VALUE => $queue->id(),
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Queue ' .  $args{'OPERATOR'}. " ". $queue->Name
+                );
+    
+}
+# }}}
+
+# {{{ sub LimitStatus
+
+=head2 LimitStatus
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of = or !=.
+VALUE is a status.
+
+=cut
+
+sub LimitStatus {
+    my $self = shift;
+    my %args = ( OPERATOR => '=',
+                  @_);
+    $self->Limit (FIELD => 'Status',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Status ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
+                );
+}
+
+# }}}
+
+# {{{ sub LimitType
+
+=head2 LimitType
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of = or !=, it defaults to "=".
+VALUE is a string to search for in the type of the ticket.
+
+=cut
+
+sub LimitType {
+    my $self = shift;
+    my %args = (OPERATOR => '=',
+               VALUE => undef,
+               @_);
+    $self->Limit (FIELD => 'Type',
+                  VALUE => $args{'VALUE'},
+                  OPERATOR => $args{'OPERATOR'},
+                  DESCRIPTION => 'Type ' .  $args{'OPERATOR'}. " ". $args{'Limit'},
+                 );
+}
+
+# }}}
+
+# }}}
+
+# {{{ Limit by string field
+
+# {{{ sub LimitSubject
+
+=head2 LimitSubject
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of = or !=.
+VALUE is a string to search for in the subject of the ticket.
+
+=cut
+
+sub LimitSubject {
+    my $self = shift;
+    my %args = (@_);
+    $self->Limit (FIELD => 'Subject',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Subject ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
+                );
+}
+
+# }}}
+
+# }}}
+
+# {{{ Limit based on ticket numerical attributes
+# Things that can be > < = !=
+
+# {{{ sub LimitId
+
+=head2 LimitId
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of =, >, < or !=.
+VALUE is a ticket Id to search for
+
+=cut
+
+sub LimitId {
+    my $self = shift;
+    my %args = (OPERATOR => '=',
+                @_);
+    
+    $self->Limit (FIELD => 'id',
+                  VALUE => $args{'VALUE'},
+                  OPERATOR => $args{'OPERATOR'},
+                  DESCRIPTION => 'Id ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
+                 );
+}
+
+# }}}
+
+# {{{ sub LimitPriority
+
+=head2 LimitPriority
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of =, >, < or !=.
+VALUE is a value to match the ticket\'s priority against
+
+=cut
+
+sub LimitPriority {
+    my $self = shift;
+    my %args = (@_);
+    $self->Limit (FIELD => 'Priority',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Priority ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
+                );
+}
+
+# }}}
+
+# {{{ sub LimitInitialPriority
+
+=head2 LimitInitialPriority
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of =, >, < or !=.
+VALUE is a value to match the ticket\'s initial priority against
+
+
+=cut
+
+sub LimitInitialPriority {
+    my $self = shift;
+    my %args = (@_);
+    $self->Limit (FIELD => 'InitialPriority',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Initial Priority ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
+                );
+}
+
+# }}}
+
+# {{{ sub LimitFinalPriority
+
+=head2 LimitFinalPriority
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of =, >, < or !=.
+VALUE is a value to match the ticket\'s final priority against
+
+=cut
+
+sub LimitFinalPriority {
+    my $self = shift;
+    my %args = (@_);
+    $self->Limit (FIELD => 'FinalPriority',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Final Priority ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
+                );
+}
+
+# }}}
+
+# {{{ sub LimitTimeWorked
+
+=head2 LimitTimeWorked
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of =, >, < or !=.
+VALUE is a value to match the ticket's TimeWorked attribute
+
+=cut
+
+sub LimitTimeWorked {
+    my $self = shift;
+    my %args = (@_);
+    $self->Limit (FIELD => 'TimeWorked',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Time worked ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
+                );
+}
+
+# }}}
+
+# {{{ sub LimitTimeLeft
+
+=head2 LimitTimeLeft
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of =, >, < or !=.
+VALUE is a value to match the ticket's TimeLeft attribute
+
+=cut
+
+sub LimitTimeLeft {
+    my $self = shift;
+    my %args = (@_);
+    $self->Limit (FIELD => 'TimeLeft',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Time left ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
+                );
+}
+
+# }}}
+
+# }}}
+
+# {{{ Limiting based on attachment attributes
+
+# {{{ sub LimitContent
+
+=head2 LimitContent
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of =, LIKE, NOT LIKE or !=.
+VALUE is a string to search for in the body of the ticket
+
+=cut
+sub LimitContent {
+    my $self = shift;
+    my %args = (@_);
+    $self->Limit (FIELD => 'Content',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Ticket content ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
+                );
+}
+
+# }}}
+# {{{ sub LimitContentType
+
+=head2 LimitContentType
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of =, LIKE, NOT LIKE or !=.
+VALUE is a content type to search ticket attachments for
+
+=cut
+  
+sub LimitContentType {
+    my $self = shift;
+    my %args = (@_);
+    $self->Limit (FIELD => 'ContentType',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Ticket content type ' .  $args{'OPERATOR'}. " ". $args{'VALUE'},
+                );
+}
+# }}}
+
+# }}}
+
+# {{{ Limiting based on people
+
+# {{{ sub LimitOwner
+
+=head2 LimitOwner
+
+Takes a paramhash with the fields OPERATOR and VALUE.
+OPERATOR is one of = or !=.
+VALUE is a user id.
+
+=cut
+
+sub LimitOwner {
+    my $self = shift;
+    my %args = ( OPERATOR => '=',
+                 @_);
+    
+    my $owner = new RT::User($self->CurrentUser);
+    $owner->Load($args{'VALUE'});
+    $self->Limit (FIELD => 'Owner',
+                 VALUE => $owner->Id,
+                 OPERATOR => $args{'OPERATOR'},
+                 DESCRIPTION => 'Owner ' .  $args{'OPERATOR'}. " ". $owner->Name()
+                );
+    
+}
+
+# }}}
+
+# {{{ Limiting watchers
+
+# {{{ sub LimitWatcher
+
+
+=head2 LimitWatcher
+  
+  Takes a paramhash with the fields OPERATOR, TYPE and VALUE.
+  OPERATOR is one of =, LIKE, NOT LIKE or !=.
+  VALUE is a value to match the ticket\'s watcher email addresses against
+  TYPE is the sort of watchers you want to match against. Leave it undef if you want to search all of them
+
+=cut
+   
+sub LimitWatcher {
+    my $self = shift;
+    my %args = ( OPERATOR => '=',
+                VALUE => undef,
+                TYPE => undef,
+               @_);
+
+
+    #build us up a description
+    my ($watcher_type, $desc);
+    if ($args{'TYPE'}) {
+       $watcher_type = $args{'TYPE'};
+    }
+    else {
+       $watcher_type = "Watcher";
+    }
+    $desc = "$watcher_type ".$args{'OPERATOR'}." ".$args{'VALUE'};
+
+
+    $self->Limit (FIELD => 'Watcher',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+                 TYPE => $args{'TYPE'},
+                 DESCRIPTION => "$desc"
+                );
+}
+
+# }}}
+
+# {{{ sub LimitRequestor
+
+=head2 LimitRequestor
+
+It\'s like LimitWatcher, but it presets TYPE to Requestor
+
+=cut
+
+
+sub LimitRequestor {
+    my $self = shift;
+    $self->LimitWatcher(TYPE=> 'Requestor', @_);
+}
+
+# }}}
+
+# {{{ sub LimitCc
+
+=head2 LimitCC
+
+It\'s like LimitWatcher, but it presets TYPE to Cc
+
+=cut
+
+sub LimitCc {
+    my $self = shift;
+    $self->LimitWatcher(TYPE=> 'Cc', @_);
+}
+
+# }}}
+
+# {{{ sub LimitAdminCc
+
+=head2 LimitAdminCc
+
+It\'s like LimitWatcher, but it presets TYPE to AdminCc
+
+=cut
+  
+sub LimitAdminCc {
+    my $self = shift;
+    $self->LimitWatcher(TYPE=> 'AdminCc', @_);
+}
+
+# }}}
+
+# }}}
+
+# }}}
+
+# {{{ Limiting based on links
+
+# {{{ LimitLinkedTo
+
+=head2 LimitLinkedTo
+
+LimitLinkedTo takes a paramhash with two fields: TYPE and TARGET
+TYPE limits the sort of relationship we want to search on
+
+TARGET is the id or URI of the TARGET of the link
+(TARGET used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as TARGET
+
+=cut
+
+sub LimitLinkedTo {
+    my $self = shift;
+    my %args = ( 
+               TICKET => undef,
+               TARGET => undef,
+               TYPE => undef,
+                @_);
+
+
+    $self->Limit( FIELD => 'LinkedTo',
+                 BASE => undef,
+                 TARGET => ($args{'TARGET'} || $args{'TICKET'}),
+                 TYPE => $args{'TYPE'},
+                 DESCRIPTION => "Tickets ".$args{'TYPE'}." by ".($args{'TARGET'} || $args{'TICKET'})
+               );
+}
+
+
+# }}}
+
+# {{{ LimitLinkedFrom
+
+=head2 LimitLinkedFrom
+
+LimitLinkedFrom takes a paramhash with two fields: TYPE and BASE
+TYPE limits the sort of relationship we want to search on
+
+
+BASE is the id or URI of the BASE of the link
+(BASE used to be 'TICKET'.  'TICKET' is deprecated, but will be treated as BASE
+
+
+=cut
+
+sub LimitLinkedFrom {
+    my $self = shift;
+    my %args = ( BASE => undef,
+                TICKET => undef,
+                TYPE => undef,
+                @_);
+
+    
+    $self->Limit( FIELD => 'LinkedTo',
+                 TARGET => undef,
+                 BASE => ($args{'BASE'} || $args{'TICKET'}),
+                 TYPE => $args{'TYPE'},
+                 DESCRIPTION => "Tickets " .($args{'BASE'} || $args{'TICKET'}) ." ".$args{'TYPE'}
+               );
+}
+
+
+# }}}
+
+# {{{ LimitMemberOf 
+sub LimitMemberOf {
+    my $self = shift;
+    my $ticket_id = shift;
+    $self->LimitLinkedTo ( TARGET=> "$ticket_id",
+                          TYPE => 'MemberOf',
+                         );
+    
+}
+# }}}
+
+# {{{ LimitHasMember
+sub LimitHasMember {
+    my $self = shift;
+    my $ticket_id =shift;
+    $self->LimitLinkedFrom ( BASE => "$ticket_id",
+                            TYPE => 'MemberOf',
+                            );
+    
+}
+# }}}
+
+# {{{ LimitDependsOn
+
+sub LimitDependsOn {
+    my $self = shift;
+    my $ticket_id = shift;
+    $self->LimitLinkedTo ( TARGET => "$ticket_id",
+                           TYPE => 'DependsOn',
+                          );
+    
+}
+
+# }}}
+
+# {{{ LimitDependedOnBy
+
+sub LimitDependedOnBy {
+    my $self = shift;
+    my $ticket_id = shift;
+    $self->LimitLinkedFrom (  BASE => "$ticket_id",
+                               TYPE => 'DependsOn',
+                            );
+    
+}
+
+# }}}
+
+
+# {{{ LimitRefersTo
+
+sub LimitRefersTo {
+    my $self = shift;
+    my $ticket_id = shift;
+    $self->LimitLinkedTo ( TARGET => "$ticket_id",
+                           TYPE => 'RefersTo',
+                          );
+    
+}
+
+# }}}
+
+# {{{ LimitReferredToBy
+
+sub LimitReferredToBy {
+    my $self = shift;
+    my $ticket_id = shift;
+    $self->LimitLinkedFrom (  BASE=> "$ticket_id",
+                               TYPE => 'RefersTo',
+                            );
+    
+}
+
+# }}}
+
+# }}}
+
+# {{{ limit based on ticket date attribtes
+
+# {{{ sub LimitDate
+
+=head2 LimitDate (FIELD => 'DateField', OPERATOR => $oper, VALUE => $ISODate)
+
+Takes a paramhash with the fields FIELD OPERATOR and VALUE.
+
+OPERATOR is one of > or < 
+VALUE is a date and time in ISO format in GMT
+FIELD is one of Starts, Started, Told, Created, Resolved, LastUpdated
+
+There are also helper functions of the form LimitFIELD that eliminate
+the need to pass in a FIELD argument.
+
+=cut
+
+sub LimitDate {
+    my $self = shift;
+    my %args = (
+                  FIELD => undef,
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+
+                  @_);
+
+    #Set the description if we didn't get handed it above
+    unless ($args{'DESCRIPTION'} ) {
+       $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
+    }
+
+    $self->Limit (%args);
+
+}
+
+# }}}
+
+
+
+
+sub LimitCreated {
+    my $self = shift;
+    $self->LimitDate( FIELD => 'Created', @_);
+}
+sub LimitDue {
+    my $self = shift;
+    $self->LimitDate( FIELD => 'Due', @_);
+
+}
+sub LimitStarts {
+    my $self = shift;
+    $self->LimitDate( FIELD => 'Starts', @_);
+
+}
+sub LimitStarted {
+    my $self = shift;
+    $self->LimitDate( FIELD => 'Started', @_);
+}
+sub LimitResolved { 
+    my $self = shift;
+    $self->LimitDate( FIELD => 'Resolved', @_);
+}
+sub LimitTold {
+    my $self = shift;
+    $self->LimitDate( FIELD => 'Told', @_);
+}
+sub LimitLastUpdated {
+    my $self = shift;
+    $self->LimitDate( FIELD => 'LastUpdated', @_);
+}
+#
+# {{{ sub LimitTransactionDate
+
+=head2 LimitTransactionDate (OPERATOR => $oper, VALUE => $ISODate)
+
+Takes a paramhash with the fields FIELD OPERATOR and VALUE.
+
+OPERATOR is one of > or < 
+VALUE is a date and time in ISO format in GMT
+
+
+=cut
+
+sub LimitTransactionDate {
+    my $self = shift;
+    my %args = (
+                  FIELD => 'TransactionDate',
+                 VALUE => $args{'VALUE'},
+                 OPERATOR => $args{'OPERATOR'},
+
+                  @_);
+
+    #Set the description if we didn't get handed it above
+    unless ($args{'DESCRIPTION'} ) {
+       $args{'DESCRIPTION'} = $args{'FIELD'} . " " .$args{'OPERATOR'}. " ". $args{'VALUE'} . " GMT"
+    }
+
+    $self->Limit (%args);
+
+}
+
+# }}}
+
+# }}}
+
+# {{{ sub LimitKeyword
+
+=head2 LimitKeyword 
+
+Takes a paramhash of key/value pairs with the following keys:
+
+=over 4
+
+=item KEYWORDSELECT - KeywordSelect id
+
+=item OPERATOR - (for KEYWORD only - KEYWORDSELECT operator is always `=')
+
+=item KEYWORD - Keyword id
+
+=back
+
+=cut
+
+sub LimitKeyword {
+    my $self = shift;
+    my %args = ( KEYWORD => undef,
+                 KEYWORDSELECT => undef,
+                OPERATOR => '=',
+                DESCRIPTION => undef,
+                FIELD => 'Keyword',
+                QUOTEVALUE => 1,
+                @_
+              );
+
+    use RT::KeywordSelect;
+    my $KeywordSelect = RT::KeywordSelect->new($self->CurrentUser);
+    $KeywordSelect->Load($args{KEYWORDSELECT});
+    
+
+    # Below, We're checking to see whether the keyword we're searching for
+    # is null or not.
+    # This could probably be rewritten to be easier to read and  understand
+
+    
+    #If we are looking to compare with a null value.
+    if ($args{'OPERATOR'} =~ /is/i)  {
+       if ($args{'OPERATOR'} =~ /^is$/i) {
+           $args{'DESCRIPTION'} ||= "Keyword Selection ". $KeywordSelect->Name . " has no value";
+       }
+       elsif ($args{'OPERATOR'} =~ /^is not$/i) {
+           $args{'DESCRIPTION'} ||= "Keyword Selection ". $KeywordSelect->Name . " has a value";
+       }
+    }
+       # if we're not looking to compare with a null value
+    else {     
+        use RT::Keyword;
+       my $Keyword = RT::Keyword->new($self->CurrentUser);
+       $Keyword->Load($args{KEYWORD});
+       $args{'DESCRIPTION'} ||= "Keyword Selection " . $KeywordSelect->Name.  " $args{OPERATOR} ". $Keyword->Name;
+    }
+    
+    $args{SingleValued} = $KeywordSelect->Single();
+    
+    my $index = $self->_NextIndex;
+    %{$self->{'TicketRestrictions'}{$index}} = %args;
+    
+    $self->{'RecalcTicketLimits'} = 1;
+    return ($index);
+}
+
+# }}}
+
+# {{{ sub _NextIndex
+
+=head2 _NextIndex
+
+Keep track of the counter for the array of restrictions
+
+=cut
+
+sub _NextIndex {
+    my $self = shift;
+    return ($self->{'restriction_index'}++);
+}
+# }}}
+
+# }}} 
+
+# {{{ Core bits to make this a DBIx::SearchBuilder object
+
+# {{{ sub _Init 
+sub _Init  {
+    my $self = shift;
+    $self->{'table'} = "Tickets";
+    $self->{'RecalcTicketLimits'} = 1;
+    $self->{'looking_at_effective_id'} = 0;
+    $self->{'restriction_index'} =1;
+    $self->{'primary_key'} = "id";
+    $self->SUPER::_Init(@_);
+
+}
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem  {
+  my $self = shift;
+  return(RT::Ticket->new($self->CurrentUser));
+
+}
+# }}}
+
+# {{{ sub Count
+sub Count {
+  my $self = shift;
+  $self->_ProcessRestrictions if ($self->{'RecalcTicketLimits'} == 1 );
+  return($self->SUPER::Count());
+}
+# }}}
+
+# {{{ sub ItemsArrayRef
+
+=head2 ItemsArrayRef
+
+Returns a reference to the set of all items found in this search
+
+=cut
+
+sub ItemsArrayRef {
+    my $self = shift;
+    my @items;
+    
+    my $placeholder = $self->_ItemsCounter;
+    $self->GotoFirstItem();
+    while (my $item = $self->Next) { 
+       push (@items, $item);
+    }
+    
+    $self->GotoItem($placeholder);
+    return(\@items);
+}
+# }}}
+
+# {{{ sub Next 
+sub Next {
+       my $self = shift;
+       
+       $self->_ProcessRestrictions if ($self->{'RecalcTicketLimits'} == 1 );
+
+       my $Ticket = $self->SUPER::Next();
+       if ((defined($Ticket)) and (ref($Ticket))) {
+
+           #Make sure we _never_ show dead tickets
+           #TODO we should be doing this in the where clause.
+           #but you can't do multiple clauses on the same field just yet :/
+
+           if ($Ticket->Status eq 'dead') {
+               return($self->Next());
+           }
+           elsif ($Ticket->CurrentUserHasRight('ShowTicket')) {
+               return($Ticket);
+           }
+
+           #If the user doesn't have the right to show this ticket
+           else {      
+               return($self->Next());
+           }
+       }
+       #if there never was any ticket
+       else {
+               return(undef);
+       }       
+
+}
+# }}}
+
+# }}}
+
+# {{{ Deal with storing and restoring restrictions
+
+# {{{ sub LoadRestrictions
+
+=head2 LoadRestrictions
+
+LoadRestrictions takes a string which can fully populate the TicketRestrictons hash.
+TODO It is not yet implemented
+
+=cut
+
+# }}}
+
+# {{{ sub DescribeRestrictions
+
+=head2 DescribeRestrictions
+
+takes nothing.
+Returns a hash keyed by restriction id. 
+Each element of the hash is currently a one element hash that contains DESCRIPTION which
+is a description of the purpose of that TicketRestriction
+
+=cut
+
+sub DescribeRestrictions  {
+    my $self = shift;
+    
+    my ($row, %listing);
+    
+    foreach $row (keys %{$self->{'TicketRestrictions'}}) {
+       $listing{$row} = $self->{'TicketRestrictions'}{$row}{'DESCRIPTION'};
+    }
+    return (%listing);
+}
+# }}}
+
+# {{{ sub RestrictionValues
+
+=head2 RestrictionValues FIELD
+
+Takes a restriction field and returns a list of values this field is restricted
+to.
+
+=cut
+
+sub RestrictionValues {
+    my $self = shift;
+    my $field = shift;
+    map $self->{'TicketRestrictions'}{$_}{'VALUE'},
+      grep {
+             $self->{'TicketRestrictions'}{$_}{'FIELD'} eq $field
+             && $self->{'TicketRestrictions'}{$_}{'OPERATOR'} eq "="
+           }
+        keys %{$self->{'TicketRestrictions'}};
+}
+
+# }}}
+
+# {{{ sub ClearRestrictions
+
+=head2 ClearRestrictions
+
+Removes all restrictions irretrievably
+
+=cut
+  
+sub ClearRestrictions {
+    my $self = shift;
+    delete $self->{'TicketRestrictions'};
+    $self->{'looking_at_effective_id'} = 0;
+    $self->{'RecalcTicketLimits'} =1;
+}
+
+# }}}
+
+# {{{ sub DeleteRestriction
+
+=head2 DeleteRestriction
+
+Takes the row Id of a restriction (From DescribeRestrictions' output, for example.
+Removes that restriction from the session's limits.
+
+=cut
+
+
+sub DeleteRestriction {
+    my $self = shift;
+    my $row = shift;
+    delete $self->{'TicketRestrictions'}{$row};
+    
+    $self->{'RecalcTicketLimits'} = 1;
+    #make the underlying easysearch object forget all its preconceptions
+}
+
+# }}}
+
+# {{{ sub _ProcessRestrictions 
+
+sub _ProcessRestrictions {
+    my $self = shift;
+
+    #Need to clean the EasySearch slate because it makes things too sticky
+    $self->CleanSlate();
+
+    #Blow away ticket aliases since we'll need to regenerate them for a new search
+    delete $self->{'TicketAliases'};
+    delete $self->{KeywordsAliases};
+
+    my $row;
+    
+    foreach $row (keys %{$self->{'TicketRestrictions'}}) {
+        my $restriction = $self->{'TicketRestrictions'}{$row};
+       # {{{ if it's an int
+       
+       if ($TYPES{$restriction->{'FIELD'}} eq 'INT' ) {
+           if ($restriction->{'OPERATOR'} =~ /^(=|!=|>|<|>=|<=)$/) {
+               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
+                             ENTRYAGGREGATOR => 'AND',
+                             OPERATOR => $restriction->{'OPERATOR'},
+                             VALUE => $restriction->{'VALUE'},
+                             );
+           }
+       }
+       # }}}
+       # {{{ if it's an enum
+       elsif ($TYPES{$restriction->{'FIELD'}} eq 'ENUM') {
+           
+           if ($restriction->{'OPERATOR'} eq '=') {
+               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
+                             ENTRYAGGREGATOR => 'OR',
+                             OPERATOR => '=',
+                             VALUE => $restriction->{'VALUE'},
+                           );
+           }
+           elsif ($restriction->{'OPERATOR'} eq '!=') {
+               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
+                             ENTRYAGGREGATOR => 'AND',
+                             OPERATOR => '!=',
+                             VALUE => $restriction->{'VALUE'},
+                           );
+           }
+           
+       }
+       # }}}
+       # {{{ if it's a date
+
+       elsif ($TYPES{$restriction->{'FIELD'}} eq 'DATE') {
+           $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
+                                ENTRYAGGREGATOR => 'AND',
+                                OPERATOR => $restriction->{'OPERATOR'},
+                                VALUE => $restriction->{'VALUE'},
+                              );
+       }
+       # }}}
+       # {{{ if it's a string
+
+       elsif ($TYPES{$restriction->{'FIELD'}} eq 'STRING') {
+           
+           if ($restriction->{'OPERATOR'} eq '=') {
+               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
+                             ENTRYAGGREGATOR => 'OR',
+                             OPERATOR => '=',
+                             VALUE => $restriction->{'VALUE'},
+                             CASESENSITIVE => 0
+                           );
+           }
+           elsif ($restriction->{'OPERATOR'} eq '!=') {
+               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
+                             ENTRYAGGREGATOR => 'AND',
+                             OPERATOR => '!=',
+                             VALUE => $restriction->{'VALUE'},
+                             CASESENSITIVE => 0
+                           );
+           }
+           elsif ($restriction->{'OPERATOR'} eq 'LIKE') {
+               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
+                             ENTRYAGGREGATOR => 'AND',
+                             OPERATOR => 'LIKE',
+                             VALUE => $restriction->{'VALUE'},
+                             CASESENSITIVE => 0
+                           );
+           }
+           elsif ($restriction->{'OPERATOR'} eq 'NOT LIKE') {
+               $self->SUPER::Limit( FIELD => $restriction->{'FIELD'},
+                             ENTRYAGGREGATOR => 'AND',
+                             OPERATOR => 'NOT LIKE',
+                             VALUE => $restriction->{'VALUE'},
+                             CASESENSITIVE => 0
+                           );
+           }
+       }
+
+       # }}}
+       # {{{ if it's Transaction content that we're hunting for
+       elsif ($TYPES{$restriction->{'FIELD'}} eq 'TRANSFIELD') {
+
+           #Basically, we want to make sure that the limits apply to the same attachment,
+           #rather than just another attachment for the same ticket, no matter how many 
+           #clauses we lump on. 
+           #We put them in TicketAliases so that they get nuked when we redo the join.
+           
+           unless (defined $self->{'TicketAliases'}{'TransFieldAlias'}) {
+               $self->{'TicketAliases'}{'TransFieldAlias'} = $self->NewAlias ('Transactions');
+           }
+           unless (defined $self->{'TicketAliases'}{'TransFieldAttachAlias'}){
+               $self->{'TicketAliases'}{'TransFieldAttachAlias'} = $self->NewAlias('Attachments');
+               
+           }
+           #Join transactions to attachments
+           $self->Join( ALIAS1 => $self->{'TicketAliases'}{'TransFieldAttachAlias'},  
+                        FIELD1 => 'TransactionId',
+                        ALIAS2 => $self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2=> 'id');
+           
+           #Join transactions to tickets
+           $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
+                        ALIAS2 =>$self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2 => 'Ticket');
+           
+           #Search for the right field
+           $self->SUPER::Limit(ALIAS => $self->{'TicketAliases'}{'TransFieldAttachAlias'},
+                                 ENTRYAGGREGATOR => 'AND',
+                                 FIELD =>    $restriction->{'FIELD'},
+                                 OPERATOR => $restriction->{'OPERATOR'} ,
+                                 VALUE =>    $restriction->{'VALUE'},
+                                 CASESENSITIVE => 0
+                               );
+           
+
+       }
+
+       # }}}
+       # {{{ if it's a Transaction date that we're hunting for
+       elsif ($TYPES{$restriction->{'FIELD'}} eq 'TRANSDATE') {
+
+           #Basically, we want to make sure that the limits apply to the same attachment,
+           #rather than just another attachment for the same ticket, no matter how many 
+           #clauses we lump on. 
+           #We put them in TicketAliases so that they get nuked when we redo the join.
+           
+           unless (defined $self->{'TicketAliases'}{'TransFieldAlias'}) {
+               $self->{'TicketAliases'}{'TransFieldAlias'} = $self->NewAlias ('Transactions');
+           }
+
+           #Join transactions to tickets
+           $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
+                        ALIAS2 =>$self->{'TicketAliases'}{'TransFieldAlias'}, FIELD2 => 'Ticket');
+           
+           #Search for the right field
+           $self->SUPER::Limit(ALIAS => $self->{'TicketAliases'}{'TransFieldAlias'},
+                               ENTRYAGGREGATOR => 'AND',
+                               FIELD =>    'Created',
+                               OPERATOR => $restriction->{'OPERATOR'} ,
+                               VALUE =>    $restriction->{'VALUE'} );
+       }
+
+       # }}}
+       # {{{ if it's a relationship that we're hunting for
+       
+       # Takes FIELD: which is something like "LinkedTo"
+       # takes TARGET or BASE which is the TARGET or BASE id that we're searching for
+       # takes TYPE which is the type of link we're looking for.
+
+       elsif ($TYPES{$restriction->{'FIELD'}} eq 'LINKFIELD') {
+
+           
+           my $LinkAlias = $self->NewAlias ('Links');
+
+           
+           #Make sure we get the right type of link, if we're restricting it
+           if ($restriction->{'TYPE'}) {
+               $self->SUPER::Limit(ALIAS => $LinkAlias,
+                                   ENTRYAGGREGATOR => 'AND',
+                                   FIELD =>   'Type',
+                                   OPERATOR => '=',
+                                   VALUE =>    $restriction->{'TYPE'} );
+           }
+           
+           #If we're trying to limit it to things that are target of
+           if ($restriction->{'TARGET'}) {
+               
+
+               # If the TARGET is an integer that means that we want to look at the LocalTarget
+               # field. otherwise, we want to look at the "Target" field
+
+               my ($matchfield);
+               if ($restriction->{'TARGET'} =~/^(\d+)$/) {
+                   $matchfield = "LocalTarget";
+               }       
+               else {
+                   $matchfield = "Target";
+               }       
+
+               $self->SUPER::Limit(ALIAS => $LinkAlias,
+                                   ENTRYAGGREGATOR => 'AND',
+                                   FIELD =>   $matchfield,
+                                   OPERATOR => '=',
+                                   VALUE =>    $restriction->{'TARGET'} );
+
+               
+               #If we're searching on target, join the base to ticket.id
+               $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
+                            ALIAS2 => $LinkAlias,
+                            FIELD2 => 'LocalBase');
+
+           
+
+
+           }
+           #If we're trying to limit it to things that are base of
+           elsif ($restriction->{'BASE'}) {
+
+
+               # If we're trying to match a numeric link, we want to look at LocalBase,
+               # otherwise we want to look at "Base"
+
+               my ($matchfield);
+               if ($restriction->{'BASE'} =~/^(\d+)$/) {
+                   $matchfield = "LocalBase";
+               }       
+               else {
+                   $matchfield = "Base";
+               }       
+
+
+               $self->SUPER::Limit(ALIAS => $LinkAlias,
+                                   ENTRYAGGREGATOR => 'AND',
+                                   FIELD => $matchfield,
+                                   OPERATOR => '=',
+                                   VALUE =>    $restriction->{'BASE'} );
+               
+               #If we're searching on base, join the target to ticket.id
+               $self->Join( ALIAS1 => 'main', FIELD1 => $self->{'primary_key'},
+                            ALIAS2 => $LinkAlias,
+                            FIELD2 => 'LocalTarget');
+               
+           }
+
+       }
+               
+       # }}}
+       # {{{ if it's a watcher that we're hunting for
+       elsif ($TYPES{$restriction->{'FIELD'}} eq 'WATCHERFIELD') {
+
+           my $Watch = $self->NewAlias('Watchers');
+
+           #Join watchers to users
+           my $User = $self->Join( TYPE => 'left',
+                                    ALIAS1 => $Watch, 
+                                    FIELD1 => 'Owner',
+                                    TABLE2 => 'Users', 
+                                    FIELD2 => 'id',
+                                  );
+
+           #Join Ticket to watchers
+           $self->Join( ALIAS1 => 'main', FIELD1 => 'id',
+                        ALIAS2 => $Watch, FIELD2 => 'Value');
+
+
+           #Make sure we're only talking about ticket watchers
+           $self->SUPER::Limit( ALIAS => $Watch,
+                                FIELD => 'Scope',
+                                VALUE => 'Ticket',
+                                OPERATOR => '=');
+
+
+           # Find email address watchers
+           $self->SUPER::Limit( SUBCLAUSE => 'WatcherEmailAddress',
+                                ALIAS => $Watch,
+                                FIELD => 'Email',
+                                ENTRYAGGREGATOR => 'OR',
+                                VALUE => $restriction->{'VALUE'},
+                                OPERATOR => $restriction->{'OPERATOR'},
+                                CASESENSITIVE => 0
+                       );
+
+
+
+           #Find user watchers
+           $self->SUPER::Limit(
+                               SUBCLAUSE => 'WatcherEmailAddress',
+                               ALIAS => $User,
+                               FIELD => 'EmailAddress',
+                               ENTRYAGGREGATOR => 'OR',
+                               VALUE => $restriction->{'VALUE'},
+                               OPERATOR => $restriction->{'OPERATOR'},
+                               CASESENSITIVE => 0
+                              );
+
+           
+           #If we only want a specific type of watchers, then limit it to that
+           if ($restriction->{'TYPE'}) {
+               $self->SUPER::Limit( ALIAS => $Watch,
+                                    FIELD => 'Type',
+                                    ENTRYAGGREGATOR => 'OR',
+                                    VALUE => $restriction->{'TYPE'},
+                                    OPERATOR => '=');
+           }
+       }
+
+       # }}}
+       # {{{ if it's a keyword
+       elsif ($TYPES{$restriction->{'FIELD'}} eq 'KEYWORDFIELD') {
+           my $null_columns_ok;
+
+            my $ObjKeywordsAlias;
+           $ObjKeywordsAlias = $self->{KeywordsAliases}{$restriction->{'KEYWORDSELECT'}}
+             if $restriction->{SingleValued};
+           unless (defined $ObjKeywordsAlias) {
+             $ObjKeywordsAlias = $self->Join(
+                                              TYPE => 'left',
+                                              ALIAS1 => 'main',
+                                              FIELD1 => 'id',
+                                              TABLE2 => 'ObjectKeywords',
+                                              FIELD2 => 'ObjectId'
+                                             );
+             if ($restriction->{'SingleValued'}) {
+               $self->{KeywordsAliases}{$restriction->{'KEYWORDSELECT'}} 
+                 = $ObjKeywordsAlias;
+             }
+           }
+
+         
+            $self->SUPER::Limit(
+                               ALIAS => $ObjKeywordsAlias,
+                               FIELD => 'Keyword',
+                               OPERATOR => $restriction->{'OPERATOR'},
+                               VALUE => $restriction->{'KEYWORD'},
+                               QUOTEVALUE => $restriction->{'QUOTEVALUE'},
+                               ENTRYAGGREGATOR => 'OR',
+                               );
+           
+            if  ( ($restriction->{'OPERATOR'} =~ /^IS$/i) or 
+                 ($restriction->{'OPERATOR'} eq '!=') ) {
+               
+               $null_columns_ok=1;
+
+           } 
+
+           #If we're trying to find tickets where the keyword isn't somethng, also check ones where it _IS_ null
+           if ( $restriction->{'OPERATOR'} eq '!=') {
+               $self->SUPER::Limit(
+                                   ALIAS => $ObjKeywordsAlias,
+                                   FIELD => 'Keyword',
+                                   OPERATOR => 'IS',
+                                   VALUE => 'NULL',
+                                   QUOTEVALUE => 0,
+                                   ENTRYAGGREGATOR => 'OR',
+                                  );
+             }
+
+
+            $self->SUPER::Limit(LEFTJOIN => $ObjKeywordsAlias,
+                               FIELD => 'KeywordSelect',
+                               VALUE => $restriction->{'KEYWORDSELECT'},
+                               ENTRYAGGREGATOR => 'OR');
+
+
+            $self->SUPER::Limit( ALIAS => $ObjKeywordsAlias,
+                                 FIELD => 'ObjectType',
+                                 VALUE => 'Ticket',
+                                 ENTRYAGGREGATOR => 'AND');
+           
+           if ($null_columns_ok) {
+                $self->SUPER::Limit(ALIAS => $ObjKeywordsAlias,
+                                    FIELD => 'ObjectType',
+                                   OPERATOR => 'IS',
+                                    VALUE => 'NULL',
+                                   QUOTEVALUE => 0,
+                                    ENTRYAGGREGATOR => 'OR');
+           }
+          
+        }
+        # }}}
+
+    
+     }
+
+     
+     # here, we make sure we don't get any tickets that have been merged  into other tickets
+     # (Ticket Id == Ticket EffectiveId
+     # note that we _really_ don't want to do this if we're already looking at the effectiveid
+     if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) {
+        $self->SUPER::Limit( FIELD => 'EffectiveId', 
+              OPERATOR => '=',
+              QUOTEVALUE => 0,
+              VALUE => 'main.id');   #TODO, we shouldn't be hard coding the tablename to main.
+      } 
+    $self->{'RecalcTicketLimits'} = 0;
+}
+
+# }}}
+
+# }}}
+
+# {{{ Deal with displaying rows of the listing 
+
+#
+#  Everything in this section is stub code for 2.2
+# It's not part of the API. It's not for your use
+# It's not for our use.
+#
+
+
+# {{{ sub SetListingFormat
+
+=head2 SetListingFormat
+
+Takes a single Format string as specified below. parses that format string and makes the various listing output
+things DTRT.
+
+=item Format strings
+
+Format strings are made up of a chain of Elements delimited with vertical pipes (|).
+Elements of a Format string 
+
+
+FormatString:    Element[::FormatString]
+
+Element:         AttributeName[;HREF=<URL>][;TITLE=<TITLE>]
+
+AttributeName    Id | Subject | Status | Owner | Priority | InitialPriority | TimeWorked | TimeLeft |
+  
+                 Keywords[;SELECT=<KeywordSelect>] | 
+       
+                <Created|Starts|Started|Contacted|Due|Resolved>Date<AsString|AsISO|AsAge>
+
+
+=cut
+
+
+
+
+#accept a format string
+
+
+
+sub SetListingFormat {
+    my $self = shift;
+    my $listing_format = shift;
+    
+    my ($element, $attribs);
+    my $i = 0;
+    foreach $element (split (/::/,$listing_format)) {
+       if ($element =~ /^(.*?);(.*)$/) {
+           $element = $1;
+           $attribs = $2;
+       }       
+       $self->{'format_string'}->[$i]->{'Element'} = $element;
+       foreach $attrib (split (/;/, $attribs)) {
+           my $value = "";
+           if ($attrib =~ /^(.*?)=(.*)$/) {
+               $attrib = $1;
+               $value = $2;
+           }   
+           $self->{'format_string'}->[$i]->{"$attrib"} = $val;
+           
+       }
+    
+    }
+    return(1);
+}
+
+# }}}
+
+# {{{ sub HeaderAsHTML
+sub HeaderAsHTML {
+    my $self = shift;
+    my $header = "";
+    my $col;
+    foreach $col ( @{[ $self->{'format_string'} ]}) {
+       $header .= "<TH>" . $self->_ColumnTitle($self->{'format_string'}->[$col]) . "</TH>";
+       
+    }
+    return ($header);
+}
+# }}}
+
+# {{{ sub HeaderAsText
+#Print text header
+sub HeaderAsText {
+    my $self = shift;
+    my ($header);
+    
+    return ($header);
+}
+# }}}
+
+# {{{ sub TicketAsHTMLRow
+#Print HTML row
+sub TicketAsHTMLRow {
+    my $self = shift;
+    my $Ticket = shift;
+    my ($row, $col);
+    foreach $col (@{[$self->{'format_string'}]}) {
+       $row .= "<TD>" . $self->_TicketColumnValue($ticket,$self->{'format_string'}->[$col]) . "</TD>";
+       
+    }
+    return ($row);
+}
+# }}}
+
+# {{{ sub TicketAsTextRow
+#Print text row
+sub TicketAsTextRow {
+    my $self = shift;
+    my ($row);
+
+    #TODO implement
+    
+    return ($row);
+}
+# }}}
+
+# {{{ _ColumnTitle {
+
+sub _ColumnTitle {
+    my $self = shift;
+    
+    # Attrib is a hash 
+    my $attrib = shift;
+    
+    # return either attrib->{'TITLE'} or..
+    if ($attrib->{'TITLE'}) {
+       return($attrib->{'TITLE'});
+    }  
+    # failing that, Look up the title in a hash
+    else {
+       #TODO create $self->{'ColumnTitles'};
+       return ($self->{'ColumnTitles'}->{$attrib->{'Element'}});
+    }  
+    
+}
+
+# }}}
+
+# {{{ _TicketColumnValue
+sub _TicketColumnValue {
+    my $self = shift;
+    my $Ticket = shift;
+    my $attrib = shift;
+
+    
+    my $out;
+
+  SWITCH: {
+       /^id/i && do {
+           $out = $Ticket->id;
+           last SWITCH; 
+       };
+       /^subj/i && do {
+           last SWITCH; 
+           $Ticket->Subject;
+                  };   
+       /^status/i && do {
+           last SWITCH; 
+           $Ticket->Status;
+       };
+       /^prio/i && do {
+           last SWITCH; 
+           $Ticket->Priority;
+       };
+       /^finalprio/i && do {
+           
+           last SWITCH; 
+           $Ticket->FinalPriority
+       };
+       /^initialprio/i && do {
+           
+           last SWITCH; 
+           $Ticket->InitialPriority;
+       };      
+       /^timel/i && do {
+           
+           last SWITCH; 
+           $Ticket->TimeWorked;
+       };
+       /^timew/i && do {
+           
+           last SWITCH; 
+           $Ticket->TimeLeft;
+       };
+       
+       /^(.*?)date(.*)$/i && do {
+           my $o = $1;
+           my $m = $2;
+           my ($obj);
+           #TODO: optimize
+           $obj = $Ticket->DueObj         if $o =~ /due/i;
+           $obj = $Ticket->CreatedObj     if $o =~ /created/i;
+           $obj = $Ticket->StartsObj      if $o =~ /starts/i;
+           $obj = $Ticket->StartedObj     if $o =~ /started/i;
+           $obj = $Ticket->ToldObj        if $o =~ /told/i;
+           $obj = $Ticket->LastUpdatedObj if $o =~ /lastu/i;
+           
+           $method = 'ISO' if $m =~ /iso/i;
+           
+           $method = 'AsString' if $m =~ /asstring/i;
+           $method = 'AgeAsString' if $m =~ /age/i;
+           last SWITCH;
+           $obj->$method();
+             
+       };
+         
+         /^watcher/i && do {
+             last SWITCH; 
+             $Ticket->WatchersAsString();
+         };    
+       
+       /^requestor/i && do {
+           last SWITCH; 
+           $Ticket->RequestorsAsString();
+       };      
+       /^cc/i && do {
+           last SWITCH; 
+           $Ticket->CCAsString();
+       };      
+       
+       
+       /^admincc/i && do {
+           last SWITCH; 
+           $Ticket->AdminCcAsString();
+       };
+       
+       /^keywords/i && do {
+           last SWITCH; 
+           #Limit it to the keyword select we're talking about, if we've got one.
+           my $objkeys =$Ticket->KeywordsObj($attrib->{'SELECT'});
+           $objkeys->KeywordRelativePathsAsString();
+       };
+       
+    }
+      
+}
+
+# }}}
+
+# }}}
+
+# {{{ POD
+=head2 notes
+"Enum" Things that get Is, IsNot
+
+
+"Int" Things that get Is LessThan and GreaterThan
+id
+InitialPriority
+FinalPriority
+Priority
+TimeLeft
+TimeWorked
+
+"Text" Things that get Is, Like
+Subject
+TransactionContent
+
+
+"Link" OPERATORs
+
+
+"Date" OPERATORs Is, Before, After
+
+  =cut
+# }}}
+1;
diff --git a/rt/lib/RT/Transaction.pm b/rt/lib/RT/Transaction.pm
new file mode 100755 (executable)
index 0000000..ee1f069
--- /dev/null
@@ -0,0 +1,783 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Transaction.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# Copyright 1999-2001 Jesse Vincent <jesse@fsck.com>
+# Released under the terms of the GNU Public License
+
+=head1 NAME
+
+  RT::Transaction - RT\'s transaction object
+
+=head1 SYNOPSIS
+
+  use RT::Transaction;
+
+
+=head1 DESCRIPTION
+
+
+Each RT::Transaction describes an atomic change to a ticket object 
+or an update to an RT::Ticket object.
+It can have arbitrary MIME attachments.
+
+
+=head1 METHODS
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::Transaction);
+
+=end testing
+
+=cut
+
+package RT::Transaction;
+
+use RT::Record;
+@ISA= qw(RT::Record);
+    
+use RT::Attachments;
+
+# {{{ sub _Init 
+sub _Init  {
+    my $self = shift;
+  $self->{'table'} = "Transactions";
+  return ($self->SUPER::_Init(@_));
+
+}
+# }}}
+
+# {{{ sub Create 
+
+=head2 Create
+
+Create a new transaction.
+
+This routine should _never_ be called anything other Than RT::Ticket. It should not be called 
+from client code. Ever. Not ever.  If you do this, we will hunt you down. and break your kneecaps.
+Then the unpleasant stuff will start.
+
+TODO: Document what gets passed to this
+
+=cut
+
+sub Create  {
+    my $self = shift;
+    my %args = ( id => undef,
+                TimeTaken => 0,
+                Ticket => 0 ,
+                Type => 'undefined',
+                Data => '',
+                Field => undef,
+                OldValue => undef,
+                NewValue => undef,
+                MIMEObj => undef,
+                ActivateScrips => 1,
+                @_
+              );
+    
+    #if we didn't specify a ticket, we need to bail
+    unless ( $args{'Ticket'} ) {
+       return(0, "RT::Transaction->Create couldn't, as you didn't specify a ticket id");
+    }
+        
+    #lets create our transaction
+    my $id = $self->SUPER::Create(Ticket => $args{'Ticket'},
+                                 TimeTaken => $args{'TimeTaken'},
+                                 Type => $args{'Type'},
+                                 Data => $args{'Data'},
+                                 Field => $args{'Field'},
+                                 OldValue => $args{'OldValue'},
+                                 NewValue => $args{'NewValue'},
+                                 Created => $args{'Created'}
+                                );
+    $self->Load($id);
+    $self->_Attach($args{'MIMEObj'})
+      if defined $args{'MIMEObj'};
+    
+    #Provide a way to turn off scrips if we need to
+    if ($args{'ActivateScrips'}) {
+
+       #We're really going to need a non-acled ticket for the scrips to work
+       my $TicketAsSystem = RT::Ticket->new($RT::SystemUser);
+       $TicketAsSystem->Load($args{'Ticket'}) || 
+         $RT::Logger->err("$self couldn't load ticket $args{'Ticket'}\n");
+       
+       my $TransAsSystem = RT::Transaction->new($RT::SystemUser);
+       $TransAsSystem->Load($self->id) ||
+         $RT::Logger->err("$self couldn't load a copy of itself as superuser\n");
+       
+       # {{{ Deal with Scrips
+    
+    #Load a scripscopes object
+    use RT::Scrips;
+    my $PossibleScrips = RT::Scrips->new($RT::SystemUser);
+    
+    $PossibleScrips->LimitToQueue($TicketAsSystem->QueueObj->Id); #Limit it to  $Ticket->QueueObj->Id
+    $PossibleScrips->LimitToGlobal(); # or to "global"
+    my $ConditionsAlias = $PossibleScrips->NewAlias('ScripConditions');
+    
+    $PossibleScrips->Join(ALIAS1 => 'main',  FIELD1 => 'ScripCondition',
+                         ALIAS2 => $ConditionsAlias, FIELD2=> 'id');
+    
+    
+    #We only want things where the scrip applies to this sort of transaction
+    $PossibleScrips->Limit(ALIAS=> $ConditionsAlias,
+                          FIELD=>'ApplicableTransTypes',
+                          OPERATOR => 'LIKE',
+                          VALUE => $args{'Type'},
+                          ENTRYAGGREGATOR => 'OR',
+                         );
+    
+    # Or where the scrip applies to any transaction
+    $PossibleScrips->Limit(ALIAS=> $ConditionsAlias,
+                          FIELD=>'ApplicableTransTypes',
+                          OPERATOR => 'LIKE',
+                          VALUE => "Any",
+                          ENTRYAGGREGATOR => 'OR',
+                         );                        
+    
+    #Iterate through each script and check it's applicability.
+    
+    while (my $Scrip = $PossibleScrips->Next()) {
+      
+      #TODO: properly deal with errors raised in this scrip loop
+       
+      #$RT::Logger->debug("$self now dealing with ".$Scrip->Id. "\n");      
+       eval {
+         local $SIG{__DIE__} = sub { $RT::Logger->error($_[0])};
+         
+         
+         #Load the scrip's Condition object
+         $Scrip->ConditionObj->LoadCondition(TicketObj => $TicketAsSystem, 
+                                             TransactionObj => $TransAsSystem);          
+         
+         
+         #If it's applicable, prepare and commit it
+         
+       $RT::Logger->debug ("$self: Checking condition ".$Scrip->ConditionObj->Name. "...\n");
+         
+         if ( $Scrip->IsApplicable() ) {
+             
+               $RT::Logger->debug ("$self: Matches condition ".$Scrip->ConditionObj->Name. "...\n");
+             #TODO: handle some errors here
+             
+             $Scrip->ActionObj->LoadAction(TicketObj => $TicketAsSystem, 
+                                          TransactionObj => $TransAsSystem);
+         
+             
+             if ($Scrip->Prepare()) {
+                 $RT::Logger->debug("$self: Prepared " .
+                                  $Scrip->ActionObj->Name . "\n");
+                 if ($Scrip->Commit()) {
+                       $RT::Logger->debug("$self: Committed " .
+                                          $Scrip->ActionObj->Name . "\n");
+                 }
+                 else {
+                       $RT::Logger->info("$self: Failed to commit ".
+                                          $Scrip->ActionObj->Name . "\n");
+                 } 
+             }
+             else {
+                 $RT::Logger->info("$self: Failed to prepare " .
+                                    $Scrip->ActionObj->Name . "\n");
+             }
+
+             #We're done with it. lets clean up.
+             #TODO: something else isn't letting these get garbage collected. check em out.
+             $Scrip->ActionObj->DESTROY();
+             $Scrip->ConditionObj->DESTROY;
+         }
+         
+         
+       else {
+           $RT::Logger->debug ("$self: Doesn't match condition ".$Scrip->ConditionObj->Name. "...\n");
+
+           # TODO: why doesn't this catch all the ScripObjs we create. 
+           # and why do we explictly need to destroy them?
+           $Scrip->ConditionObj->DESTROY;
+       }
+      }        
+    }
+
+    # }}}
+       
+    }
+
+    return ($id, "Transaction Created");
+}
+
+# }}}
+
+# {{{ sub Delete
+
+sub Delete {
+    my $self = shift;
+    return (0, 'Deleting this object could break referential integrity');
+}
+
+# }}}
+
+# {{{ Routines dealing with Attachments
+
+# {{{ sub Message 
+
+=head2 Message
+
+  Returns the RT::Attachments Object which contains the "top-level" object
+  attachment for this transaction
+
+=cut
+
+sub Message  {
+
+    my $self = shift;
+    
+    if (!defined ($self->{'message'}) ){
+       
+       $self->{'message'} = new RT::Attachments($self->CurrentUser);
+       $self->{'message'}->Limit(FIELD => 'TransactionId',
+                                 VALUE => $self->Id);
+       
+       $self->{'message'}->ChildrenOf(0);
+    } 
+    return($self->{'message'});
+}
+# }}}
+
+# {{{ sub Content
+
+=head2 Content PARAMHASH
+
+If this transaction has attached mime objects, returns the first text/ part.
+Otherwise, returns undef.
+
+Takes a paramhash.  If the $args{'Quote'} parameter is set, wraps this message 
+at $args{'Wrap'}.  $args{'Wrap'} defaults to 70.
+
+
+=cut
+
+sub Content {
+    my $self = shift;
+    my %args = ( Quote => 0,
+                Wrap => 70,
+                @_ );
+
+    my $content = undef;
+
+    # If we don\'t have any content, return undef now.
+    unless ($self->Message->First) {
+       return (undef);
+    }  
+    
+    # Get the set of toplevel attachments to this transaction.
+    my $MIMEObj = $self->Message->First();
+    
+    # If it's a message or a plain part, just return the
+    # body. 
+    if ($MIMEObj->ContentType() =~ '^(text|message)(/|$)') {
+       $content = $MIMEObj->Content();
+    }
+    
+    # If it's a multipart object, first try returning the first 
+    # text/plain part. 
+    
+    elsif ($MIMEObj->ContentType() =~ '^multipart/') {
+       my $plain_parts = $MIMEObj->Children();
+       $plain_parts->ContentType(VALUE => 'text/plain');
+       
+       # If we actully found a part, return its content
+       if ($plain_parts->First && 
+        $plain_parts->First->Content ne '') {
+           $content = $plain_parts->First->Content;            
+       }       
+       
+       # If that fails, return the  first text/ or message/ part 
+       # which has some content.
+    
+       else {
+           my $all_parts = $MIMEObj->Children();
+           while (($content == undef) && 
+                  (my $part = $all_parts->Next)) {
+               if (($part->ContentType() =~ '^(text|message)(/|$)') and
+                   ($part->Content())) {
+                   $content = $part->Content;
+               }       
+           }
+       }       
+
+    }
+    # If all else fails, return a message that we couldn't find
+    # any content
+    else { 
+        $content = 'This transaction appears to have no content';
+    }  
+
+    if ($args{'Quote'}) {
+       # Remove quoted signature.
+       $content =~ s/\n-- \n(.*)$//s;
+
+       # What's the longest line like?
+       foreach (split (/\n/,$content)) {
+           $max=length if ( length > $max);
+       }
+
+       if ($max>76) {
+           require Text::Wrapper;
+           my $wrapper=new Text::Wrapper
+               (
+                columns => $args{'Wrap'}, 
+                body_start => ($max > 70*3 ? '   ' : ''),
+                par_start => ''
+                );
+           $content=$wrapper->wrap($content);
+       }
+
+       $content =~ s/^/> /gm;
+       $content = '[' . $self->CreatorObj->Name() . ' - ' . $self->CreatedAsString()
+                   . "]:\n\n"
+               . $content . "\n\n";
+
+    }
+
+    return ($content); 
+}
+# }}}
+
+# {{{ sub Subject
+
+=head2 Subject
+
+If this transaction has attached mime objects, returns the first one's subject
+Otherwise, returns null
+  
+=cut
+
+sub Subject {
+    my $self = shift;
+    if ($self->Message->First) {
+       return ($self->Message->First->Subject);
+    }
+    else {
+       return (undef);
+    }
+}
+# }}}
+
+# {{{ sub Attachments 
+
+=head2 Attachments
+
+  Returns all the RT::Attachment objects which are attached
+to this transaction. Takes an optional parameter, which is
+a ContentType that Attachments should be restricted to.
+
+=cut
+
+
+sub Attachments  {
+    my $self = shift;
+    my $Types = '';
+    $Types = shift if (@_);
+
+    my $Attachments = new RT::Attachments($self->CurrentUser);
+    
+    #If it's a comment, return an empty object if they don't have the right to see it
+    if ($self->Type eq 'Comment') {
+       unless ($self->CurrentUserHasRight('ShowTicketComments')) {
+           return ($Attachments);
+       }
+    }  
+    #if they ain't got rights to see, return an empty object
+    else {
+       unless ($self->CurrentUserHasRight('ShowTicket')) {
+           return ($Attachments);
+       }
+    }
+    
+    $Attachments->Limit(FIELD => 'TransactionId',
+                       VALUE => $self->Id);
+
+    # Get the attachments in the order they're put into
+    # the database.  Arguably, we should be returning a tree
+    # of attachments, not a set...but no current app seems to need
+    # it. 
+
+    $Attachments->OrderBy(ALIAS => 'main', 
+                         FIELD => 'Id',
+                         ORDER => 'asc');
+
+    if ($Types) {
+       $Attachments->ContentType( VALUE => "$Types",
+                                  OPERATOR => "LIKE");
+    }
+    
+    
+    return($Attachments);
+    
+}
+
+# }}}
+
+# {{{ sub _Attach 
+
+=head2 _Attach
+
+A private method used to attach a mime object to this transaction.
+
+=cut
+
+sub _Attach  {
+    my $self = shift;
+    my $MIMEObject = shift;
+    
+    if (!defined($MIMEObject)) {
+       $RT::Logger->error("$self _Attach: We can't attach a mime object if you don't give us one.\n");
+       return(0, "$self: no attachment specified");
+    }
+    
+  
+    use RT::Attachment;
+    my $Attachment = new RT::Attachment ($self->CurrentUser);
+    $Attachment->Create(TransactionId => $self->Id,
+                       Attachment => $MIMEObject);
+    return ($Attachment, "Attachment created");
+    
+}
+
+# }}}
+
+# }}}
+
+# {{{ Routines dealing with Transaction Attributes
+
+# {{{ sub TicketObj
+
+=head2 TicketObj
+
+Returns this transaction's ticket object.
+
+=cut
+
+sub TicketObj {
+    my $self = shift;
+    if (! exists $self->{'TicketObj'}) {
+       $self->{'TicketObj'} = new RT::Ticket($self->CurrentUser);
+       $self->{'TicketObj'}->Load($self->Ticket);
+    }
+    
+    return $self->{'TicketObj'};
+}
+# }}}
+
+# {{{ sub Description 
+
+=head2 Description
+
+Returns a text string which describes this transaction
+
+=cut
+
+
+sub Description  {
+    my $self = shift;
+
+    #Check those ACLs
+    #If it's a comment, we need to be extra special careful
+    if ($self->__Value('Type') eq 'Comment') {
+       unless ($self->CurrentUserHasRight('ShowTicketComments')) {
+           return (0, "Permission Denied");
+       }
+    }  
+
+    #if they ain't got rights to see, don't let em
+    else {
+       unless ($self->CurrentUserHasRight('ShowTicket')) {
+           return (0, "Permission Denied");
+       }
+    }
+
+    if (!defined($self->Type)) {
+       return("No transaction type specified");
+    }
+    
+    return ($self->BriefDescription . " by " . $self->CreatorObj->Name);
+}
+
+# }}}
+
+# {{{ sub BriefDescription 
+
+=head2 BriefDescription
+
+Returns a text string which briefly describes this transaction
+
+=cut
+
+
+sub BriefDescription  {
+    my $self = shift;
+
+    #Check those ACLs
+    #If it's a comment, we need to be extra special careful
+    if ($self->__Value('Type') eq 'Comment') {
+       unless ($self->CurrentUserHasRight('ShowTicketComments')) {
+           return (0, "Permission Denied");
+       }
+    }  
+
+    #if they ain't got rights to see, don't let em
+    else {
+       unless ($self->CurrentUserHasRight('ShowTicket')) {
+           return (0, "Permission Denied");
+       }
+    }
+
+    if (!defined($self->Type)) {
+       return("No transaction type specified");
+    }
+    
+    if ($self->Type eq 'Create'){
+       return("Ticket created");
+    }
+    elsif ($self->Type =~ /Status/) {
+       if ($self->Field eq 'Status') {
+           if ($self->NewValue eq 'dead') {
+               return ("Ticket killed");
+      }
+           else {
+               return( "Status changed from ".  $self->OldValue . 
+                       " to ". $self->NewValue);
+
+           }
+       }
+       # Generic:
+       return ($self->Field." changed from ".($self->OldValue||"(empty value)").
+         " to ".$self->NewValue );
+      }
+    
+    if ($self->Type eq 'Correspond')    {
+       return("Correspondence added");
+    }
+    
+    elsif ($self->Type eq 'Comment')  {
+       return( "Comments added");
+    }
+    
+    elsif ($self->Type eq 'Keyword') {
+
+       my $field = 'Keyword';
+
+       if ($self->Field) {
+           my $keywordsel = new RT::KeywordSelect ($self->CurrentUser);
+           $keywordsel->Load($self->Field);
+           $field = $keywordsel->Name();
+       }
+
+       if ($self->OldValue eq '') {
+           return ($field." ".$self->NewValue." added");
+       }
+       elsif ($self->NewValue eq '') {
+           return ($field." ".$self->OldValue." deleted"); 
+           
+       }
+       else {
+           return ($field." ".$self->OldValue . " changed to ". 
+                    $self->NewValue);
+       }       
+    }
+    
+    elsif ($self->Type eq 'Untake'){
+           return( "Untaken");
+       }
+    
+    elsif ($self->Type eq "Take") {
+       return( "Taken");
+    }
+    
+    elsif ($self->Type eq "Force") {
+        my $Old = RT::User->new($self->CurrentUser);
+        $Old->Load($self->OldValue);
+        my $New = RT::User->new($self->CurrentUser);
+        $New->Load($self->NewValue);
+       return "Owner forcibly changed from ".$Old->Name . " to ". $New->Name;
+    }
+    elsif ($self->Type eq "Steal") {
+       my $Old = RT::User->new($self->CurrentUser);
+       $Old->Load($self->OldValue);
+       return "Stolen from ".$Old->Name;
+    }
+    
+    elsif ($self->Type eq "Give") {
+       my $New = RT::User->new($self->CurrentUser);
+       $New->Load($self->NewValue);
+       return( "Given to ".$New->Name);
+    }
+    
+    elsif ($self->Type eq 'AddWatcher'){
+       return( $self->Field." ". $self->NewValue ." added");
+    }
+    
+    elsif ($self->Type eq 'DelWatcher'){
+       return( $self->Field." ".$self->OldValue ." deleted");
+    }
+    
+    elsif ($self->Type eq 'Subject') {
+       return( "Subject changed to ".$self->Data);
+    }
+    elsif ($self->Type eq 'Told') {
+       return( "User notified");
+    }
+    
+    elsif ($self->Type eq 'AddLink') {
+       return ($self->Data);
+    }
+    elsif ($self->Type eq 'DeleteLink') {
+       return ($self->Data);
+    }
+    elsif ($self->Type eq 'Set') {
+       if ($self->Field eq 'Queue') {
+           my $q1 = new RT::Queue($self->CurrentUser);
+           $q1->Load($self->OldValue);
+           my $q2 = new RT::Queue($self->CurrentUser);
+           $q2->Load($self->NewValue);
+           return ($self->Field . " changed from " . $q1->Name . " to ".
+                   $q2->Name);
+       }
+
+        # Write the date/time change at local time:                    
+    elsif ($self->Field =~  /Due|Starts|Started|Told/) {           
+        my $t1 = new RT::Date($self->CurrentUser);                 
+        $t1->Set(Format => 'ISO', Value => $self->NewValue);       
+        my $t2 = new RT::Date($self->CurrentUser);                 
+        $t2->Set(Format => 'ISO', Value => $self->OldValue);       
+        return ($self->Field . " changed from " . $t2->AsString .  
+                    " to ".$t1->AsString);      
+    }                
+       else {
+           return ($self->Field . " changed from " . $self->OldValue . 
+                   " to ".$self->NewValue);
+       }       
+    }
+    elsif ($self->Type eq 'PurgeTransaction') {
+       return ("Transaction ".$self->Data. " purged");
+    }
+    else {
+       return ("Default: ". $self->Type ."/". $self->Field . 
+               " changed from " . $self->OldValue . 
+               " to ".$self->NewValue);
+       
+    }
+}
+
+# }}}
+
+# {{{ Utility methods
+
+# {{{ sub IsInbound
+
+=head2 IsInbound
+
+Returns true if the creator of the transaction is a requestor of the ticket.
+Returns false otherwise
+
+=cut
+
+sub IsInbound {
+    my $self=shift;
+    return ($self->TicketObj->IsRequestor($self->CreatorObj));
+}
+
+# }}}
+
+# }}}
+
+# {{{ sub _Accessible 
+
+sub _Accessible  {
+  my $self = shift;
+  my %Cols = (
+             TimeTaken => 'read',
+             Ticket => 'read/public',
+             Type=> 'read',
+             Field => 'read',
+             Data => 'read',
+             NewValue => 'read',
+             OldValue => 'read',
+             Creator => 'read/auto',
+             Created => 'read/auto',
+            );
+  return $self->SUPER::_Accessible(@_, %Cols);
+}
+
+# }}}
+
+# }}}
+
+# {{{ sub _Set
+
+sub _Set {
+    my $self = shift;
+    return(0, 'Transactions are immutable');
+}
+
+# }}}
+
+# {{{ sub _Value 
+
+=head2 _Value
+
+Takes the name of a table column.
+Returns its value as a string, if the user passes an ACL check
+
+=cut
+
+sub _Value  {
+
+    my $self = shift;
+    my $field = shift;
+    
+    
+    #if the field is public, return it.
+    if ($self->_Accessible($field, 'public')) {
+       return($self->__Value($field));
+       
+    }
+    #If it's a comment, we need to be extra special careful
+    if ($self->__Value('Type') eq 'Comment') {
+       unless ($self->CurrentUserHasRight('ShowTicketComments')) {
+           return (undef);
+       }
+    }  
+    #if they ain't got rights to see, don't let em
+    else {
+       unless ($self->CurrentUserHasRight('ShowTicket')) {
+           return (undef);
+       }
+    }  
+    
+    return($self->__Value($field));
+    
+}
+
+# }}}
+
+# {{{ sub CurrentUserHasRight
+
+=head2 CurrentUserHasRight RIGHT
+
+Calls $self->CurrentUser->HasQueueRight for the right passed in here.
+passed in here.
+
+=cut
+
+sub CurrentUserHasRight {
+    my $self = shift;
+    my $right = shift;
+    return ($self->CurrentUser->HasQueueRight(Right => "$right", 
+                                              TicketObj => $self->TicketObj));            
+}
+
+# }}}
+
+1;
diff --git a/rt/lib/RT/Transactions.pm b/rt/lib/RT/Transactions.pm
new file mode 100755 (executable)
index 0000000..2ae98f2
--- /dev/null
@@ -0,0 +1,78 @@
+#$Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Transactions.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+
+=head1 NAME
+
+  RT::Transactions - a collection of RT Transaction objects
+
+=head1 SYNOPSIS
+
+  use RT::Transactions;
+
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing
+
+ok (require RT::TestHarness);
+ok (require RT::Transactions);
+
+=end testing
+
+=cut
+
+package RT::Transactions;
+use RT::EasySearch;
+
+@ISA= qw(RT::EasySearch);
+use RT::Transaction;
+
+# {{{ sub _Init  
+sub _Init   {
+  my $self = shift;
+  
+  $self->{'table'} = "Transactions";
+  $self->{'primary_key'} = "id";
+  
+  # By default, order by the date of the transaction, rather than ID.
+  $self->OrderBy( ALIAS => 'main',
+                 FIELD => 'Created',
+                 ORDER => 'ASC');
+
+  return ( $self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub NewItem 
+sub NewItem  {
+    my $self = shift;
+    
+    return(RT::Transaction->new($self->CurrentUser));
+}
+# }}}
+
+
+=head2 example methods
+
+  Queue RT::Queue or Queue Id
+  Ticket RT::Ticket or Ticket Id
+
+
+LimitDate 
+  
+Type TRANSTYPE
+Field STRING
+OldValue OLDVAL
+NewValue NEWVAL
+Data DATA
+TimeTaken
+Actor USEROBJ/USERID
+ContentMatches STRING
+
+=cut
+
+
+1;
+
diff --git a/rt/lib/RT/User.pm b/rt/lib/RT/User.pm
new file mode 100755 (executable)
index 0000000..4e85540
--- /dev/null
@@ -0,0 +1,1222 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/User.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-2000 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+
+=head1 NAME
+
+  RT::User - RT User object
+
+=head1 SYNOPSIS
+
+  use RT::User;
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::User);
+
+=end testing
+
+
+=cut
+
+
+package RT::User;
+use RT::Record;
+@ISA= qw(RT::Record);
+
+# {{{ sub _Init
+sub _Init  {
+    my $self = shift;
+    $self->{'table'} = "Users";
+    return($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub _Accessible 
+
+sub _Accessible  {
+  my $self = shift;
+  my %Cols = (
+             # {{{ Core RT info
+             Name => 'public/read/write/admin',
+             Password => 'write',
+             Comments => 'read/write/admin',
+             Signature => 'read/write',
+             EmailAddress => 'public/read/write',
+             PagerEmailAddress => 'read/write',
+             FreeformContactInfo => 'read/write',
+             Organization => 'public/read/write/admin',
+             Disabled => 'public/read/write/admin', #To modify this attribute, we have helper
+             #methods
+             Privileged => 'read/write/admin', # 0=no 1=user 2=system
+
+             # }}}
+             
+             # {{{ Names
+             
+             RealName => 'public/read/write',
+             NickName => 'public/read/write',
+             # }}}
+                     
+             # {{{ Localization and Internationalization
+             Lang => 'public/read/write',
+             EmailEncoding => 'public/read/write',
+             WebEncoding => 'public/read/write',
+             # }}}
+             
+             # {{{ External ContactInfo Linkage
+             ExternalContactInfoId => 'public/read/write/admin',
+             ContactInfoSystem => 'public/read/write/admin',
+             # }}}
+             
+             # {{{ User Authentication identifier
+             ExternalAuthId => 'public/read/write/admin',
+             #Authentication system used for user 
+             AuthSystem => 'public/read/write/admin',
+             Gecos => 'public/read/write/admin', #Gecos is the name of the fields in a 
+             # unix passwd file. In this case, it refers to "Unix Username"
+             # }}}
+             
+             # {{{ Telephone numbers
+             HomePhone =>  'read/write',
+             WorkPhone => 'read/write',
+             MobilePhone => 'read/write',
+             PagerPhone => 'read/write',
+
+             # }}}
+             
+             # {{{ Paper Address
+             Address1 => 'read/write',
+             Address2 => 'read/write',
+             City => 'read/write',
+             State => 'read/write',
+             Zip => 'read/write',
+             Country => 'read/write',
+             # }}}
+             
+             # {{{ Core DBIx::Record Attributes
+             Creator => 'read/auto',
+             Created => 'read/auto',
+             LastUpdatedBy => 'read/auto',
+             LastUpdated => 'read/auto'
+
+             # }}}
+            );
+  return($self->SUPER::_Accessible(@_, %Cols));
+}
+
+# }}}
+
+# {{{ sub Create 
+
+sub Create  {
+    my $self = shift;
+    my %args = (Privileged => 0,
+               @_ # get the real argumentlist
+              );
+    
+    #Check the ACL
+    unless ($self->CurrentUserHasRight('AdminUsers')) {
+       return (0, 'No permission to create users');
+    }
+    
+    if (! $args{'Password'})  {
+       $args{'Password'} = '*NO-PASSWORD*';
+    }
+    elsif (length($args{'Password'}) < $RT::MinimumPasswordLength) {
+        return(0,"Password too short");
+    }
+    else {
+        my $salt = join '', ('.','/',0..9,'A'..'Z','a'..'z')[rand 64, rand 64];
+        $args{'Password'} = crypt($args{'Password'}, $salt);     
+    }   
+        
+    
+    #TODO Specify some sensible defaults.
+    
+    unless (defined ($args{'Name'})) {
+       return(0, "Must specify 'Name' attribute");
+    }  
+    
+    
+    #SANITY CHECK THE NAME AND ABORT IF IT'S TAKEN
+    if ($RT::SystemUser) { #This only works if RT::SystemUser has been defined
+               my $TempUser = RT::User->new($RT::SystemUser);
+               $TempUser->Load($args{'Name'});
+               return (0, 'Name in use') if ($TempUser->Id);
+       
+               return(0, 'Email address in use') 
+                       unless ($self->ValidateEmailAddress($args{'EmailAddress'}));
+    }
+    else {
+               $RT::Logger->warning("$self couldn't check for pre-existing ".
+                            " users on create. This will happen".
+                            " on installation\n");
+    }
+    
+    my $id = $self->SUPER::Create(%args);
+    
+    #If the create failed.
+    unless ($id) {
+               return (0, 'Could not create user');
+    }
+      
+    
+    #TODO post 2.0
+    #if ($args{'SendWelcomeMessage'}) {
+    #  #TODO: Check if the email exists and looks valid
+    #  #TODO: Send the user a "welcome message" 
+    #}
+    
+    return ($id, 'User created');
+}
+
+# }}}
+
+# {{{ sub _BootstrapCreate 
+
+#create a user without validating _any_ data.
+
+#To be used only on database init.
+
+sub _BootstrapCreate {
+    my $self = shift;
+    my %args = (@_);
+
+    $args{'Password'} = "*NO-PASSWORD*";
+    my $id = $self->SUPER::Create(%args);
+    
+    #If the create failed.
+    return (0, 'Could not create user') 
+      unless ($id);
+
+    return ($id, 'User created');
+}
+
+# }}}
+
+# {{{ sub Delete 
+
+sub Delete  {
+    my $self = shift;
+    
+    return(0, 'Deleting this object would violate referential integrity');
+    
+}
+
+# }}}
+
+# {{{ sub Load 
+
+=head2 Load
+
+Load a user object from the database. Takes a single argument.
+If the argument is numerical, load by the column 'id'. Otherwise, load by
+the "Name" column which is the user's textual username.
+
+=cut
+
+sub Load  {
+    my $self = shift;
+    my $identifier = shift || return undef;
+    
+    #if it's an int, load by id. otherwise, load by name.
+    if ($identifier !~ /\D/) {
+       $self->SUPER::LoadById($identifier);
+    }
+    else {
+       $self->LoadByCol("Name",$identifier);
+    }
+}
+
+# }}}
+
+
+# {{{ sub LoadByEmail
+
+=head2 LoadByEmail
+
+Tries to load this user object from the database by the user's email address.
+
+
+=cut
+
+sub LoadByEmail {
+    my $self=shift;
+    my $address = shift;
+
+    # Never load an empty address as an email address.
+    unless ($address) {
+       return(undef);
+    }
+
+    $address = RT::CanonicalizeAddress($address);
+    #$RT::Logger->debug("Trying to load an email address: $address\n");
+    return $self->LoadByCol("EmailAddress", $address);
+}
+# }}}
+
+
+# {{{ sub ValidateEmailAddress
+
+=head2 ValidateEmailAddress ADDRESS
+
+Returns true if the email address entered is not in use by another user or is 
+undef or ''. Returns false if it's in use. 
+
+=cut
+
+sub ValidateEmailAddress {
+       my $self = shift;
+       my $Value = shift;
+
+       # if the email address is null, it's always valid
+       return (1) if(!$Value || $Value eq "");
+
+       my $TempUser = RT::User->new($RT::SystemUser);
+       $TempUser->LoadByEmail($Value);
+
+       if( $TempUser->id && 
+          ($TempUser->id != $self->id)) { # if we found a user with that address 
+                                       # it's invalid to set this user's address to it
+               return(undef);
+       }
+       else { #it's a valid email address
+               return(1);
+       }
+}
+
+# }}}
+
+
+
+
+# {{{ sub SetRandomPassword
+
+=head2 SetRandomPassword
+
+Takes no arguments. Returns a status code and a new password or an error message.
+If the status is 1, the second value returned is the new password.
+If the status is anything else, the new value returned is the error code.
+
+=cut
+
+sub SetRandomPassword  {
+    my $self = shift;
+
+
+    unless ($self->CurrentUserCanModify('Password')) {
+       return (0, "Permission Denied");
+    }
+    
+    my $pass = $self->GenerateRandomPassword(6,8);
+
+    # If we have "notify user on 
+
+    my ($val, $msg) = $self->SetPassword($pass);
+    
+    #If we got an error return the error.
+    return (0, $msg) unless ($val);
+    
+    #Otherwise, we changed the password, lets return it.
+    return (1, $pass);
+    
+}
+
+# }}}
+
+
+# {{{ sub ResetPassword
+
+=head2 ResetPassword
+
+Returns status, [ERROR or new password].  Resets this user\'s password to
+a randomly generated pronouncable password and emails them, using a 
+global template called "RT_PasswordChange", which can be overridden
+with global templates "RT_PasswordChange_Privileged" or "RT_PasswordChange_NonPrivileged" 
+for privileged and Non-privileged users respectively.
+
+=cut
+
+sub ResetPassword {
+    my $self = shift;
+    
+    unless ($self->CurrentUserCanModify('Password')) {
+       return (0, "Permission Denied");
+    }
+    my ($status, $pass) = $self->SetRandomPassword();
+
+    unless ($status) {
+       return (0, "$pass");
+    }
+    
+    my $template = RT::Template->new($self->CurrentUser);
+
+
+    if ($self->IsPrivileged) {
+       $template->LoadGlobalTemplate('RT_PasswordChange_Privileged');
+    } 
+    else {
+       $template->LoadGlobalTemplate('RT_PasswordChange_Privileged');
+    }  
+    
+    unless ($template->Id) {
+       $template->LoadGlobalTemplate('RT_PasswordChange');
+    }  
+    
+    unless ($template->Id) {
+       $RT::Logger->crit("$self tried to send ".$self->Name." a password reminder ".
+                         "but couldn't find a password change template");
+    }  
+
+    my $notification =  RT::Action::SendPasswordEmail->new(TemplateObj => $template,
+                                                          Argument => $pass);
+    
+    $notification->SetTo($self->EmailAddress);
+
+    my ($ret);
+    $ret = $notification->Prepare();
+    if ($ret) {
+       $ret = $notification->Commit();
+    }
+    
+    if ($ret) {
+       return(1, 'New password notification sent');
+    }  else {
+       return (0, 'Notification could not be sent');
+    }  
+    
+}
+
+
+# }}}
+
+# {{{ sub GenerateRandomPassword
+
+=head2 GenerateRandomPassword MIN_LEN and MAX_LEN
+
+Returns a random password between MIN_LEN and MAX_LEN characters long.
+
+=cut
+
+sub GenerateRandomPassword {
+    my $self = shift;
+    my $min_length = shift;
+    my $max_length = shift;
+    
+    #This code derived from mpw.pl, a bit of code with a sordid history
+    # Its notes: 
+    
+    # Perl cleaned up a bit by Jesse Vincent 1/14/2001.
+    # Converted to perl from C by Marc Horowitz, 1/20/2000.
+    # Converted to C from Multics PL/I by Bill Sommerfeld, 4/21/86.
+    # Original PL/I version provided by Jerry Saltzer.
+
+    
+    my ($frequency, $start_freq, $total_sum, $row_sums);
+
+    #When munging characters, we need to know where to start counting letters from
+    my $a = ord('a');
+
+    # frequency of English digraphs (from D Edwards 1/27/66) 
+    $frequency =
+      [ [ 4, 20, 28, 52, 2, 11, 28, 4, 32, 4, 6, 62, 23,
+         167, 2, 14, 0, 83, 76, 127, 7, 25, 8, 1, 9, 1 ], # aa - az
+       [ 13, 0, 0, 0, 55, 0, 0, 0, 8, 2, 0, 22, 0,
+         0, 11, 0, 0, 15, 4, 2, 13, 0, 0, 0, 15, 0 ], # ba - bz
+       [ 32, 0, 7, 1, 69, 0, 0, 33, 17, 0, 10, 9, 1,
+         0, 50, 3, 0, 10, 0, 28, 11, 0, 0, 0, 3, 0 ], # ca - cz
+       [ 40, 16, 9, 5, 65, 18, 3, 9, 56, 0, 1, 4, 15,
+         6, 16, 4, 0, 21, 18, 53, 19, 5, 15, 0, 3, 0 ], # da - dz
+       [ 84, 20, 55, 125, 51, 40, 19, 16, 50, 1, 4, 55, 54,
+         146, 35, 37, 6, 191, 149, 65, 9, 26, 21, 12, 5, 0 ], # ea - ez
+       [ 19, 3, 5, 1, 19, 21, 1, 3, 30, 2, 0, 11, 1,
+         0, 51, 0, 0, 26, 8, 47, 6, 3, 3, 0, 2, 0 ], # fa - fz
+       [ 20, 4, 3, 2, 35, 1, 3, 15, 18, 0, 0, 5, 1,
+         4, 21, 1, 1, 20, 9, 21, 9, 0, 5, 0, 1, 0 ], # ga - gz
+       [ 101, 1, 3, 0, 270, 5, 1, 6, 57, 0, 0, 0, 3,
+         2, 44, 1, 0, 3, 10, 18, 6, 0, 5, 0, 3, 0 ], # ha - hz
+       [ 40, 7, 51, 23, 25, 9, 11, 3, 0, 0, 2, 38, 25,
+         202, 56, 12, 1, 46, 79, 117, 1, 22, 0, 4, 0, 3 ], # ia - iz
+       [ 3, 0, 0, 0, 5, 0, 0, 0, 1, 0, 0, 0, 0,
+         0, 4, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0 ], # ja - jz
+       [ 1, 0, 0, 0, 11, 0, 0, 0, 13, 0, 0, 0, 0,
+         2, 0, 0, 0, 0, 6, 2, 1, 0, 2, 0, 1, 0 ], # ka - kz
+       [ 44, 2, 5, 12, 62, 7, 5, 2, 42, 1, 1, 53, 2,
+         2, 25, 1, 1, 2, 16, 23, 9, 0, 1, 0, 33, 0 ], # la - lz
+       [ 52, 14, 1, 0, 64, 0, 0, 3, 37, 0, 0, 0, 7,
+         1, 17, 18, 1, 2, 12, 3, 8, 0, 1, 0, 2, 0 ], # ma - mz
+       [ 42, 10, 47, 122, 63, 19, 106, 12, 30, 1, 6, 6, 9,
+         7, 54, 7, 1, 7, 44, 124, 6, 1, 15, 0, 12, 0 ], # na - nz
+       [ 7, 12, 14, 17, 5, 95, 3, 5, 14, 0, 0, 19, 41,
+         134, 13, 23, 0, 91, 23, 42, 55, 16, 28, 0, 4, 1 ], # oa - oz
+       [ 19, 1, 0, 0, 37, 0, 0, 4, 8, 0, 0, 15, 1,
+         0, 27, 9, 0, 33, 14, 7, 6, 0, 0, 0, 0, 0 ], # pa - pz
+       [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+         0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0 ], # qa - qz
+       [ 83, 8, 16, 23, 169, 4, 8, 8, 77, 1, 10, 5, 26,
+         16, 60, 4, 0, 24, 37, 55, 6, 11, 4, 0, 28, 0 ], # ra - rz
+       [ 65, 9, 17, 9, 73, 13, 1, 47, 75, 3, 0, 7, 11,
+         12, 56, 17, 6, 9, 48, 116, 35, 1, 28, 0, 4, 0 ], # sa - sz
+       [ 57, 22, 3, 1, 76, 5, 2, 330, 126, 1, 0, 14, 10,
+         6, 79, 7, 0, 49, 50, 56, 21, 2, 27, 0, 24, 0 ], # ta - tz
+       [ 11, 5, 9, 6, 9, 1, 6, 0, 9, 0, 1, 19, 5,
+         31, 1, 15, 0, 47, 39, 31, 0, 3, 0, 0, 0, 0 ], # ua - uz
+       [ 7, 0, 0, 0, 72, 0, 0, 0, 28, 0, 0, 0, 0,
+         0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0 ], # va - vz
+       [ 36, 1, 1, 0, 38, 0, 0, 33, 36, 0, 0, 4, 1,
+         8, 15, 0, 0, 0, 4, 2, 0, 0, 1, 0, 0, 0 ], # wa - wz
+       [ 1, 0, 2, 0, 0, 1, 0, 0, 3, 0, 0, 0, 0,
+         0, 1, 5, 0, 0, 0, 3, 0, 0, 1, 0, 0, 0 ], # xa - xz
+       [ 14, 5, 4, 2, 7, 12, 12, 6, 10, 0, 0, 3, 7,
+         5, 17, 3, 0, 4, 16, 30, 0, 0, 5, 0, 0, 0 ], # ya - yz
+       [ 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0,
+         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ]; # za - zz
+
+    #We need to know the totals for each row 
+    $row_sums =
+      [ map { my $sum = 0; map { $sum += $_ } @$_; $sum } @$frequency ];
+    
+
+    #Frequency with which a given letter starts a word.
+    $start_freq =
+      [ 1299, 425, 725, 271, 375, 470, 93, 223, 1009, 24, 20, 355, 379,
+       319, 823, 618, 21, 317, 962, 1991, 271, 104, 516, 6, 16, 14 ];
+    
+    $total_sum = 0; map { $total_sum += $_ } @$start_freq;
+    
+    
+    my $length = $min_length + int(rand($max_length-$min_length));
+    
+    my $char = $self->GenerateRandomNextChar($total_sum, $start_freq);
+    my @word = ($char+$a);
+    for (2..$length) {
+       $char = $self->_GenerateRandomNextChar($row_sums->[$char], $frequency->[$char]);
+       push(@word, $char+$a);
+    }
+    
+    #Return the password
+    return pack("C*",@word);
+    
+}
+
+
+#A private helper function for RandomPassword
+# Takes a row summary and a frequency chart for the next character to be searched
+sub _GenerateRandomNextChar {
+    my $self = shift;
+    my($all, $freq) = @_;
+    my($pos, $i);
+    
+    for ($pos = int(rand($all)), $i=0;
+        $pos >= $freq->[$i];
+        $pos -= $freq->[$i], $i++) {};
+    
+    return($i);
+}
+
+# }}}
+
+# {{{ sub SetPassword
+
+=head2 SetPassword
+
+Takes a string. Checks the string's length and sets this user's password 
+to that string.
+
+=cut
+
+sub SetPassword {
+    my $self = shift;
+    my $password = shift;
+    
+    unless ($self->CurrentUserCanModify('Password')) {
+       return(0, 'Permission Denied');
+    }
+    
+    if (! $password)  {
+        return(0, "No password set");
+    }
+    elsif (length($password) < $RT::MinimumPasswordLength) {
+        return(0,"Password too short");
+    }
+    else {
+        my $salt = join '', ('.','/',0..9,'A'..'Z','a'..'z')[rand 64, rand 64];
+        return ( $self->SUPER::SetPassword(crypt($password, $salt)) );
+    }   
+    
+}
+
+# }}}
+
+# {{{ sub IsPassword 
+
+=head2 IsPassword
+
+Returns true if the passed in value is this user's password.
+Returns undef otherwise.
+
+=cut
+
+sub IsPassword { 
+    my $self = shift;
+    my $value = shift;
+
+    #TODO there isn't any apparent way to legitimately ACL this
+
+    # RT does not allow null passwords 
+    if ((!defined ($value)) or ($value eq '')) {
+       return(undef);
+    } 
+    if ($self->Disabled) {
+       $RT::Logger->info("Disabled user ".$self->Name." tried to log in");
+       return(undef);
+    }
+
+    if ( ($self->__Value('Password') eq '') || 
+         ($self->__Value('Password') eq undef) )  {
+        return(undef);
+     }
+    if ($self->__Value('Password') eq crypt($value, $self->__Value('Password'))) {
+       return (1);
+    }
+    else {
+       return (undef);
+    }
+}
+
+# }}}
+
+# {{{ sub SetDisabled
+
+=head2 Sub SetDisabled
+
+Toggles the user's disabled flag.
+If this flag is
+set, all password checks for this user will fail. All ACL checks for this
+user will fail. The user will appear in no user listings.
+
+=cut 
+
+# }}}
+
+# {{{ ACL Related routines
+
+# {{{ GrantQueueRight
+
+=head2 GrantQueueRight
+
+Grant a queue right to this user.  Takes a paramhash of which the elements
+RightAppliesTo and RightName are important.
+
+=cut
+
+sub GrantQueueRight {
+    
+    my $self = shift;
+    my %args = ( RightScope => 'Queue',
+                RightName => undef,
+                RightAppliesTo => undef,
+                PrincipalType => 'User',
+                PrincipalId => $self->Id,
+                @_);
+   
+    #ACL check handled in ACE.pm
+
+    require RT::ACE;
+
+#    $RT::Logger->debug("$self ->GrantQueueRight right:". $args{'RightName'} .
+#                     " applies to queue ".$args{'RightAppliesTo'}."\n");
+    
+    my $ace = new RT::ACE($self->CurrentUser);
+    
+    return ($ace->Create(%args));
+}
+
+# }}}
+
+# {{{ GrantSystemRight
+
+=head2 GrantSystemRight
+
+Grant a system right to this user. 
+The only element that's important to set is RightName.
+
+=cut
+sub GrantSystemRight {
+    
+    my $self = shift;
+    my %args = ( RightScope => 'System',
+                RightName => undef,
+                RightAppliesTo => 0,
+                PrincipalType => 'User',
+                PrincipalId => $self->Id,
+                @_);
+   
+
+    #ACL check handled in ACE.pm
+
+    require RT::ACE;    
+    my $ace = new RT::ACE($self->CurrentUser);
+    
+    return ($ace->Create(%args));
+}
+
+
+# }}}
+
+# {{{ sub HasQueueRight
+
+=head2 HasQueueRight
+
+Takes a paramhash which can contain
+these items:
+    TicketObj => RT::Ticket or QueueObj => RT::Queue or Queue => integer
+    IsRequestor => undef, (for bootstrapping create)
+    Right => 'Right' 
+
+
+Returns 1 if this user has the right specified in the paramhash. for the queue
+passed in.
+
+Returns undef if they don't
+
+=cut
+
+sub HasQueueRight {
+    my $self = shift;
+    my %args = ( TicketObj => undef,
+                 QueueObj => undef,
+                Queue => undef,
+                IsRequestor => undef,
+                Right => undef,
+                @_);
+    
+    my ($IsRequestor, $IsCc, $IsAdminCc, $IsOwner);
+    
+    if (defined $args{'Queue'}) {
+       $args{'QueueObj'} = new RT::Queue($self->CurrentUser);
+       $args{'QueueObj'}->Load($args{'Queue'});
+    }
+    
+    if (defined $args{'TicketObj'}) {
+       $args{'QueueObj'} = $args{'TicketObj'}->QueueObj();
+    }
+
+    # {{{ Validate and load up the QueueId
+    unless ((defined $args{'QueueObj'}) and ($args{'QueueObj'}->Id)) {
+       require Carp;
+       $RT::Logger->debug(Carp::cluck ("$self->HasQueueRight Couldn't find a queue id"));
+       return undef;
+    }
+
+    # }}}
+
+        
+    # Figure out whether a user has the right we're asking about.
+    # first see if they have the right personally for the queue in question. 
+    my $retval = $self->_HasRight(Scope => 'Queue',
+                                 AppliesTo => $args{'QueueObj'}->Id,
+                                 Right => $args{'Right'},
+                                 IsOwner => $IsOwner);
+
+    return ($retval) if (defined $retval);
+    
+    # then we see whether they have the right personally globally. 
+    $retval = $self->HasSystemRight( $args{'Right'});
+
+    return ($retval) if (defined $retval);
+    
+    # now that we know they don't have the right personally,
+    
+    # {{{ Find out about whether the current user is a Requestor, Cc, AdminCc or Owner
+
+    if (defined $args{'TicketObj'}) {
+       if ($args{'TicketObj'}->IsRequestor($self)) {#user is requestor
+           $IsRequestor = 1;
+       }       
+
+       if ($args{'TicketObj'}->IsCc($self)) { #If user is a cc
+           $IsCc = 1;
+       }
+
+       if ($args{'TicketObj'}->IsAdminCc($self)) { #If user is an admin cc
+           $IsAdminCc = 1;
+       }       
+       
+       if ($args{'TicketObj'}->IsOwner($self)) { #If user is an owner
+           $IsOwner = 1;
+       }
+    }
+    
+    if (defined $args{'QueueObj'}) {
+       if ($args{'QueueObj'}->IsCc($self)) { #If user is a cc
+           $IsCc = 1;
+       }
+       if ($args{'QueueObj'}->IsAdminCc($self)) { #If user is an admin cc
+           $IsAdminCc = 1;
+       }
+       
+    } 
+    # }}}
+    
+    # then see whether they have the right for the queue as a member of a metagroup 
+
+    $retval = $self->_HasRight(Scope => 'Queue',
+                                 AppliesTo => $args{'QueueObj'}->Id,
+                                 Right => $args{'Right'},
+                                 IsOwner => $IsOwner,
+                                 IsCc => $IsCc,
+                                 IsAdminCc => $IsAdminCc,
+                                 IsRequestor => $IsRequestor
+                                );
+
+    return ($retval) if (defined $retval);
+
+    #   then we see whether they have the right globally as a member of a metagroup
+    $retval = $self->HasSystemRight( $args{'Right'},
+                                    (IsOwner => $IsOwner,
+                                     IsCc => $IsCc,
+                                     IsAdminCc => $IsAdminCc,
+                                     IsRequestor => $IsRequestor
+                                    ) );
+
+    #If they haven't gotten it by now, they just lose.
+    return ($retval);
+    
+}
+
+# }}}
+  
+# {{{ sub HasSystemRight
+
+=head2 HasSystemRight
+
+takes an array of a single value and a paramhash.
+The single argument is the right being passed in.
+the param hash is some additional data. (IsCc, IsOwner, IsAdminCc and IsRequestor)
+
+Returns 1 if this user has the listed 'right'. Returns undef if this user doesn't.
+
+=cut
+
+sub HasSystemRight {
+    my $self = shift;
+    my $right = shift;
+
+    my %args = ( IsOwner => undef,
+                IsCc => undef,
+                IsAdminCc => undef,
+                IsRequestor => undef,
+                @_);
+    
+    unless (defined $right) {
+
+       $RT::Logger->debug("$self RT::User::HasSystemRight was passed in no right.");
+       return(undef);
+    }  
+    return ( $self->_HasRight ( Scope => 'System',
+                               AppliesTo => '0',
+                               Right => $right,
+                               IsOwner => $args{'IsOwner'},
+                               IsCc => $args{'IsCc'},
+                               IsAdminCc => $args{'IsAdminCc'},
+                               IsRequestor => $args{'IsRequestor'},
+                               
+                             )
+          );
+    
+}
+
+# }}}
+
+# {{{ sub _HasRight
+
+=head2 sub _HasRight (Right => 'right', Scope => 'scope',  AppliesTo => int, ExtendedPrincipals => SQL)
+
+_HasRight is a private helper method for checking a user's rights. It takes
+several options:
+
+=item Right is a textual right name
+
+=item Scope is a textual scope name. (As of July these were Queue, Ticket and System
+
+=item AppliesTo is the numerical Id of the object identified in the scope. For tickets, this is the queue #. for queues, this is the queue #
+
+=item ExtendedPrincipals is an  SQL select clause which assumes that the only
+table in play is ACL.  It's used by HasQueueRight to pass in which 
+metaprincipals apply. Actually, it's probably obsolete. TODO: remove it.
+
+Returns 1 if a matching ACE was found.
+
+Returns undef if no ACE was found.
+
+=cut
+
+
+sub _HasRight {
+    
+    my $self = shift;
+    my %args = ( Right => undef,
+                Scope => undef,
+                AppliesTo => undef,
+                IsRequestor => undef,
+                IsCc => undef,
+                IsAdminCc => undef,
+                IsOwner => undef,
+                ExtendedPrincipals => undef,
+                @_);
+    
+    if ($self->Disabled) {
+       $RT::Logger->debug ("Disabled User:  ".$self->Name.
+                           " failed access check for ".$args{'Right'}.
+                           " to object ".$args{'Scope'}."/".
+                           $args{'AppliesTo'}."\n");
+       return (undef);
+    }
+    
+    if (!defined $args{'Right'}) {
+       $RT::Logger->debug("_HasRight called without a right\n");
+       return(undef);
+    }
+    elsif (!defined $args{'Scope'}) {
+       $RT::Logger->debug("_HasRight called without a scope\n");
+       return(undef);
+    }
+    elsif (!defined $args{'AppliesTo'}) {
+       $RT::Logger->debug("_HasRight called without an AppliesTo object\n");
+       return(undef);
+    }
+    
+    #If we've cached a win or loss for this lookup say so
+    
+    #TODO Security +++ check to make sure this is complete and right
+    
+    #Construct a hashkey to cache decisions in
+    my ($hashkey);
+    { #it's ugly, but we need to turn off warning, cuz we're joining nulls.
+       local $^W=0;
+       $hashkey =$self->Id .":". join(':',%args);
+    }  
+    
+  # $RT::Logger->debug($hashkey."\n");
+    
+    #Anything older than 10 seconds needs to be rechecked
+    my $cache_timeout = (time - 10);
+    
+    
+    if ((defined $self->{'rights'}{"$hashkey"}) &&
+           ($self->{'rights'}{"$hashkey"} == 1 ) &&
+        (defined $self->{'rights'}{"$hashkey"}{'set'} ) &&
+           ($self->{'rights'}{"$hashkey"}{'set'} > $cache_timeout)) {
+#        $RT::Logger->debug("Cached ACL win for ". 
+#                           $args{'Right'}.$args{'Scope'}.
+#                           $args{'AppliesTo'}."\n");      
+       return ($self->{'rights'}{"$hashkey"});
+    }
+    elsif ((defined $self->{'rights'}{"$hashkey"}) &&
+              ($self->{'rights'}{"$hashkey"} == -1)  &&
+           (defined $self->{'rights'}{"$hashkey"}{'set'}) &&
+              ($self->{'rights'}{"$hashkey"}{'set'} > $cache_timeout)) {
+       
+#      $RT::Logger->debug("Cached ACL loss decision for ". 
+#                         $args{'Right'}.$args{'Scope'}.
+#                         $args{'AppliesTo'}."\n");        
+       
+       return(undef);
+    }
+    
+    
+    my $RightClause = "(RightName = '$args{'Right'}')";
+    my $ScopeClause = "(RightScope = '$args{'Scope'}')";
+    
+    #If an AppliesTo was passed in, we should pay attention to it.
+    #otherwise, none is needed
+    
+    $ScopeClause = "($ScopeClause AND (RightAppliesTo = $args{'AppliesTo'}))"
+      if ($args{'AppliesTo'});
+    
+    
+    # The generic principals clause looks for users with my id
+    # and Rights that apply to _everyone_
+    my $PrincipalsClause = "((PrincipalType = 'User') AND (PrincipalId = ".$self->Id."))";
+    
+    
+    # If the user is the superuser, grant them the damn right ;)
+    my $SuperUserClause = 
+      "(RightName = 'SuperUser') AND (RightScope = 'System') AND (RightAppliesTo = 0)";
+    
+    # If we've been passed in an extended principals clause, we should lump it
+    # on to the existing principals clause. it'll make life easier
+    if ($args{'ExtendedPrincipals'}) {
+       $PrincipalsClause = "(($PrincipalsClause) OR ".
+         "($args{'ExtendedPrincipalsClause'}))";
+    }
+    
+    my $GroupPrincipalsClause = "((ACL.PrincipalType = 'Group') ".
+      "AND (ACL.PrincipalId = Groups.Id) AND (GroupMembers.GroupId = Groups.Id) ".
+     " AND (GroupMembers.UserId = ".$self->Id."))";
+    
+    
+
+
+    # {{{ A bunch of magic statements that make the metagroups listed
+    # work. basically, we if the user falls into the right group,
+    # we add the type of ACL check needed
+    my (@MetaPrincipalsSubClauses, $MetaPrincipalsClause);
+    
+    #The user is always part of the 'Everyone' Group
+    push (@MetaPrincipalsSubClauses,  "((Groups.Name = 'Everyone') AND 
+                                       (PrincipalType = 'Group') AND 
+                                       (Groups.Id = PrincipalId))");
+
+    if ($args{'IsAdminCc'}) {
+       push (@MetaPrincipalsSubClauses,  "((Groups.Name = 'AdminCc') AND 
+                                       (PrincipalType = 'Group') AND 
+                                       (Groups.Id = PrincipalId))");
+    }
+    if ($args{'IsCc'}) {
+       push (@MetaPrincipalsSubClauses, " ((Groups.Name = 'Cc') AND 
+                                       (PrincipalType = 'Group') AND 
+                                       (Groups.Id = PrincipalId))");
+    }
+    if ($args{'IsRequestor'}) {
+       push (@MetaPrincipalsSubClauses,  " ((Groups.Name = 'Requestor') AND 
+                                       (PrincipalType = 'Group') AND 
+                                       (Groups.Id = PrincipalId))");
+    }
+    if ($args{'IsOwner'}) {
+       
+       push (@MetaPrincipalsSubClauses, " ((Groups.Name = 'Owner') AND 
+                                       (PrincipalType = 'Group') AND 
+                                       (Groups.Id = PrincipalId))");
+    }
+
+    # }}}
+    
+    my ($GroupRightsQuery, $MetaGroupRightsQuery, $IndividualRightsQuery, $hitcount);
+    
+    # {{{ If there are any metaprincipals to be checked
+    if (@MetaPrincipalsSubClauses) {
+       #chop off the leading or
+       #TODO redo this with an array and a join
+       $MetaPrincipalsClause = join (" OR ", @MetaPrincipalsSubClauses);
+       
+       $MetaGroupRightsQuery =  "SELECT COUNT(ACL.id) FROM ACL, Groups".
+         " WHERE " .
+           " ($ScopeClause) AND ($RightClause) AND ($MetaPrincipalsClause)";
+       
+       # {{{ deal with checking if the user has a right as a member of a metagroup
+
+#      $RT::Logger->debug("Now Trying $MetaGroupRightsQuery\n");       
+       $hitcount = $self->_Handle->FetchResult($MetaGroupRightsQuery);
+       
+       #if there's a match, the right is granted
+       if ($hitcount) {
+           $self->{'rights'}{"$hashkey"}{'set'} = time;
+           $self->{'rights'}{"$hashkey"} = 1;
+           return (1);
+       }
+       
+#      $RT::Logger->debug("No ACL matched MetaGroups query: $MetaGroupRightsQuery\n"); 
+
+       # }}}    
+       
+    }
+    # }}}
+
+    # {{{ deal with checking if the user has a right as a member of a group
+    # This query checks to se whether the user has the right as a member of a
+    # group
+    $GroupRightsQuery = "SELECT COUNT(ACL.id) FROM ACL, GroupMembers, Groups".
+      " WHERE " .
+       " (((($ScopeClause) AND ($RightClause)) OR ($SuperUserClause)) ".
+         " AND ($GroupPrincipalsClause))";    
+    
+    #  $RT::Logger->debug("Now Trying $GroupRightsQuery\n");   
+    $hitcount = $self->_Handle->FetchResult($GroupRightsQuery);
+    
+    #if there's a match, the right is granted
+    if ($hitcount) {
+       $self->{'rights'}{"$hashkey"}{'set'} = time;
+       $self->{'rights'}{"$hashkey"} = 1;
+       return (1);
+    }
+    
+#    $RT::Logger->debug("No ACL matched $GroupRightsQuery\n"); 
+    
+    # }}}
+
+    # {{{ Check to see whether the user has a right as an individual
+    
+    # This query checks to see whether the current user has the right directly
+    $IndividualRightsQuery = "SELECT COUNT(ACL.id) FROM ACL WHERE ".
+      " ((($ScopeClause) AND ($RightClause)) OR ($SuperUserClause)) " .
+       " AND ($PrincipalsClause)";
+
+    
+    $hitcount = $self->_Handle->FetchResult($IndividualRightsQuery);
+    
+    if ($hitcount) {
+       $self->{'rights'}{"$hashkey"}{'set'} = time;
+       $self->{'rights'}{"$hashkey"} = 1;
+       return (1);
+    }
+    # }}}
+
+    else { #If the user just doesn't have the right
+       
+#      $RT::Logger->debug("No ACL matched $IndividualRightsQuery\n");
+       
+       #If nothing matched, return 0.
+       $self->{'rights'}{"$hashkey"}{'set'} = time;
+       $self->{'rights'}{"$hashkey"} = -1;
+
+       
+       return (undef);
+    }
+}
+
+# }}}
+
+# {{{ sub CurrentUserCanModify
+
+=head2 CurrentUserCanModify RIGHT
+
+If the user has rights for this object, either because
+he has 'AdminUsers' or (if he\'s trying to edit himself and the right isn\'t an 
+admin right) 'ModifySelf', return 1. otherwise, return undef.
+
+=cut
+
+sub CurrentUserCanModify {
+    my $self = shift;
+    my $right = shift;
+
+    if ($self->CurrentUserHasRight('AdminUsers')) {
+       return (1);
+    }
+    #If the field is marked as an "administrators only" field, 
+    # don\'t let the user touch it.
+    elsif ($self->_Accessible($right, 'admin')) {
+       return(undef);
+    }
+    
+    #If the current user is trying to modify themselves
+    elsif ( ($self->id == $self->CurrentUser->id)  and
+           ($self->CurrentUserHasRight('ModifySelf'))) {
+       return(1);
+    }
+    #If we don\'t have a good reason to grant them rights to modify
+    # by now, they lose
+    else {
+       return(undef);
+    }
+    
+}
+
+# }}}
+
+# {{{ sub CurrentUserHasRight
+
+=head2 CurrentUserHasRight
+  
+  Takes a single argument. returns 1 if $Self->CurrentUser
+  has the requested right. returns undef otherwise
+
+=cut
+
+sub CurrentUserHasRight {
+    my $self = shift;
+    my $right = shift;
+    
+    return ($self->CurrentUser->HasSystemRight($right));
+}
+
+# }}}
+
+
+# {{{ sub _Set
+
+sub _Set {
+  my $self = shift;
+  
+  my %args = (Field => undef,
+             Value => undef,
+             @_
+            );
+
+  # Nobody is allowed to futz with RT_System or Nobody unless they
+  # want to change an email address. For 2.2, neither should have an email address
+
+  if ($self->Privileged == 2) {
+    return (0, "Can not modify system users"); 
+  }
+  unless ($self->CurrentUserCanModify($args{'Field'})) {
+      return (0, "Permission Denied");
+  }
+
+
+  
+  #Set the new value
+  my ($ret, $msg)=$self->SUPER::_Set(Field => $args{'Field'}, 
+                                    Value=> $args{'Value'});
+  
+    return ($ret, $msg);
+}
+
+# }}}
+
+# {{{ sub _Value 
+
+=head2 _Value
+
+Takes the name of a table column.
+Returns its value as a string, if the user passes an ACL check
+
+=cut
+
+sub _Value  {
+
+  my $self = shift;
+  my $field = shift;
+  
+  #If the current user doesn't have ACLs, don't let em at it.  
+  
+  my @PublicFields = qw( Name EmailAddress Organization Disabled
+                        RealName NickName Gecos ExternalAuthId 
+                        AuthSystem ExternalContactInfoId 
+                        ContactInfoSystem );
+
+  #if the field is public, return it.
+  if ($self->_Accessible($field, 'public')) {
+      return($self->SUPER::_Value($field));
+      
+  }
+  #If the user wants to see their own values, let them
+  elsif ($self->CurrentUser->Id == $self->Id) {        
+      return($self->SUPER::_Value($field));
+  } 
+  #If the user has the admin users right, return the field
+  elsif ($self->CurrentUserHasRight('AdminUsers')) {
+      return($self->SUPER::_Value($field));
+  }
+  else {
+      return(undef);
+  }    
+
+}
+
+# }}}
+
+# }}}
+1;
diff --git a/rt/lib/RT/Users.pm b/rt/lib/RT/Users.pm
new file mode 100755 (executable)
index 0000000..f4a9726
--- /dev/null
@@ -0,0 +1,281 @@
+
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Users.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-1999 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+
+=head1 NAME
+
+  RT::Users - Collection of RT::User objects
+
+=head1 SYNOPSIS
+
+  use RT::Users;
+
+
+=head1 DESCRIPTION
+
+
+=head1 METHODS
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::Users);
+
+=end testing
+
+=cut
+
+package RT::Users;
+use RT::EasySearch;
+@ISA = qw(RT::EasySearch);
+
+# {{{ sub _Init 
+sub _Init  {
+  my $self = shift;
+  $self->{'table'} = "Users";
+  $self->{'primary_key'} = "id";
+
+  # By default, order by name
+  $self->OrderBy( ALIAS => 'main',
+                 FIELD => 'Name',
+                 ORDER => 'ASC');
+
+  return ($self->SUPER::_Init(@_));
+  
+}
+# }}}
+
+# {{{ sub _DoSearch 
+
+=head2 _DoSearch
+
+  A subclass of DBIx::SearchBuilder::_DoSearch that makes sure that _Disabled rows never get seen unless
+we're explicitly trying to see them.
+
+=cut
+
+sub _DoSearch {
+    my $self = shift;
+    
+    #unless we really want to find disabled rows, make sure we\'re only finding enabled ones.
+    unless($self->{'find_disabled_rows'}) {
+       $self->LimitToEnabled();
+    }
+    
+    return($self->SUPER::_DoSearch(@_));
+    
+}
+
+# }}}
+
+# {{{ sub NewItem 
+
+sub NewItem  {
+  my $self = shift;
+
+  use RT::User;
+  my $item = new RT::User($self->CurrentUser);
+  return($item);
+}
+# }}}
+
+# {{{ LimitToEmail
+=head2 LimitToEmail
+
+Takes one argument. an email address. limits the returned set to
+that email address
+
+=cut
+
+sub LimitToEmail {
+    my $self = shift;
+    my $addr = shift;
+    $self->Limit(FIELD => 'EmailAddress', VALUE => "$addr");
+}
+
+# }}}
+
+# {{{ MemberOfGroup
+
+=head2 MemberOfGroup
+
+takes one argument, a group id number. Limits the returned set
+to members of a given group
+
+=cut
+
+sub MemberOfGroup {
+    my $self = shift;
+    my $group = shift;
+    
+    return ("No group specified") if (!defined $group);
+
+    my $groupalias = $self->NewAlias('GroupMembers');
+
+    $self->Join( ALIAS1 => 'main', FIELD1 => 'id', 
+                ALIAS2 => "$groupalias", FIELD2 => 'Name');
+    
+    $self->Limit (ALIAS => "$groupalias",
+                 FIELD => 'GroupId',
+                 VALUE => "$group",
+                 OPERATOR => "="
+                );
+}
+
+# }}}
+
+# {{{ LimitToPrivileged
+
+=head2 LimitToPrivileged
+
+Limits to users who can be made members of ACLs and groups
+
+=cut
+
+sub LimitToPrivileged {
+    my $self = shift;
+    $self->Limit( FIELD => 'Privileged',
+                  OPERATOR => '=',
+                  VALUE => '1');
+}
+
+# }}}
+
+
+
+# {{{ LimitToSystem
+
+=head2 LimitToSystem
+
+Limits to users who can be granted rights, but who should
+never have their rights modified by a user or be made members of groups.
+
+=cut
+
+sub LimitToSystem {
+    my $self = shift;
+    $self->Limit( FIELD => 'Privileged',
+                  OPERATOR => '=',
+                  VALUE => '2');
+}
+
+# }}}
+
+# {{{ HasQueueRight
+
+=head2 HasQueueRight
+
+Takes a queue id as its first argument.  Queue Id "0" is treated by RT as "applies to all queues"
+Takes a specific right as an optional second argument
+
+Limits the returned set to users who have rights in the queue specified, personally.  If the optional second argument is supplied, limits to users who have been explicitly granted that right.
+
+
+
+This should not be used as an ACL check, but only for obtaining lists of
+users with explicit rights in a given queue.
+
+=cut
+
+sub HasQueueRight {
+    my $self = shift;
+    my $queue = shift;
+    my $right;
+    
+    $right = shift if (@_);
+
+
+    my $acl_alias  = $self->NewAlias('ACL');
+    $self->Join( ALIAS1 => 'main',  FIELD1 => 'id',
+                ALIAS2 => $acl_alias, FIELD2 => 'PrincipalId');
+    $self->Limit (ALIAS => $acl_alias,
+                FIELD => 'PrincipalType',
+                OPERATOR => '=',
+                VALUE => 'User');
+
+
+    $self->Limit(ALIAS => $acl_alias,
+                FIELD => 'RightAppliesTo',
+                OPERATOR => '=',
+                VALUE => "$queue");
+
+
+    $self->Limit(ALIAS => $acl_alias,
+                FIELD => 'RightScope',
+                OPERATOR => '=',
+                ENTRYAGGREGATOR => 'OR',
+                VALUE => 'Queue');
+
+
+    $self->Limit(ALIAS => $acl_alias,
+                FIELD => 'RightScope',
+                OPERATOR => '=',
+                ENTRYAGGREGATOR => 'OR',
+                VALUE => 'Ticket');
+
+
+    #TODO: is this being initialized properly if the right isn't there?
+    if (defined ($right)) {
+       
+       $self->Limit(ALIAS => $acl_alias,
+                    FIELD => 'RightName',
+                    OPERATOR => '=',
+                    VALUE => "$right");
+       
+       
+       };
+
+
+}
+
+
+
+# }}}
+
+# {{{ HasSystemRight
+
+=head2 HasSystemRight
+
+Takes one optional argument:
+   The name of a System level right.
+
+Limits the returned set to users who have been granted system rights, personally.  If the optional argument is passed in, limits to users who have been granted the explicit right listed.   Please see the note attached to LimitToQueueRights
+
+=cut
+
+sub HasSystemRight {
+    my $self = shift;
+    my $right = shift if (@_);
+       my $acl_alias  = $self->NewAlias('ACL');
+
+
+    $self->Join( ALIAS1 => 'main',  FIELD1 => 'id',
+                ALIAS2 => $acl_alias, FIELD2 => 'PrincipalId');
+    $self->Limit (ALIAS => $acl_alias,
+                FIELD => 'PrincipalType',
+                OPERATOR => '=',
+                VALUE => 'User');
+
+    $self->Limit(ALIAS => $acl_alias,
+                FIELD => 'RightScope',
+                OPERATOR => '=',
+                VALUE => 'System');
+
+
+    #TODO: is this being initialized properly if the right isn't there?
+    if (defined ($right)) {
+       $self->Limit(ALIAS => $acl_alias,
+                    FIELD => 'RightName',
+                    OPERATOR => '=',
+                    VALUE => "$right");
+       
+       }
+
+    
+}
+
+# }}}
+
+1;
+
diff --git a/rt/lib/RT/Watcher.pm b/rt/lib/RT/Watcher.pm
new file mode 100755 (executable)
index 0000000..c7c6100
--- /dev/null
@@ -0,0 +1,313 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attic/Watcher.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-2001 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+
+=head1 NAME
+
+  RT::Watcher - RT Watcher object
+
+=head1 SYNOPSIS
+
+  use RT::Watcher;
+
+
+=head1 DESCRIPTION
+
+This module should never be called directly by client code. it\'s an internal module which
+should only be accessed through exported APIs in Ticket, Queue and other similar objects.
+
+=head1 METHODS
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::Watcher);
+
+=end testing
+
+=cut
+
+package RT::Watcher;
+use RT::Record;
+@ISA= qw(RT::Record);
+
+
+# {{{ sub _Init 
+
+sub _Init {
+  my $self = shift;
+  
+  $self->{'table'} = "Watchers";
+  return ($self->SUPER::_Init(@_));
+
+}
+# }}}
+
+# {{{ sub Create 
+
+=head2 Create PARAMHASH
+
+Create a new watcher object with the following Attributes:
+
+Scope:  Ticket or Queue
+Value: Ticket or queue id
+Type: Requestor, Cc or AdminCc.  Requestor is not supported for a scope of \'Queue\'
+Email: The email address of the watcher.  If the email address maps to an RT User, this is resolved
+to an Owner object instead.
+Owner: The RT user id of the \'owner\' of this watcher object. 
+
+=cut
+
+sub Create  {
+    my $self = shift;
+    my %args = (
+               Owner => undef,
+               Email => undef,
+               Value => undef,
+               Scope => undef,
+               Type => undef,
+               Quiet => 0,
+               @_ # get the real argumentlist
+              );
+    
+    #Do we have someone this applies to?
+    unless (($args{'Owner'} =~ /^(\d+)$/) || ($args{'Email'} =~ /\@/)) {
+       return (0, "No user or email address specified");
+    }
+    
+    #if we only have an email address, try to resolve it to an owner
+    if ($args{'Owner'} == 0) {
+        my $User = new RT::User($RT::SystemUser);
+        $User->LoadByEmail($args{'Email'});
+        if ($User->id) {
+            $args{'Owner'} = $User->id;
+           delete $args{'Email'};
+       }
+    }
+    
+    
+    if ($args{'Type'} eq "Requestor" and $args{'Owner'} == 0) {
+       # Requestors *MUST* have an account
+       
+       my $Address = RT::CanonicalizeAddress($args{'Email'});
+       
+       my $NewUser = RT::User->new($RT::SystemUser);
+       my ($Val, $Message) =
+         $NewUser->Create(Name => $Address,
+                          EmailAddress => $Address,
+                          RealName => $Address,
+                          Password => undef,
+                          Privileged => 0,
+                          Comments => 'Autocreated on ticket submission'
+                         );
+       return (0, "Could not create watcher for requestor")
+         unless $Val;
+       if ($NewUser->id) {
+           $args{'Owner'} = $NewUser->id;
+           delete $args{'Email'};
+       }
+    }
+    
+    
+    
+    
+    #Make sure we\'ve got a valid type
+    #TODO --- move this to ValidateType 
+    return (0, "Invalid Type")
+      unless ($args{'Type'} =~ /^(Requestor|Cc|AdminCc)$/i);
+
+    my $id = $self->SUPER::Create(%args);
+    if ($id) {
+       return (1,"Interest noted");
+    }
+    else {
+       return (0, "Error adding watcher");
+    }
+}
+# }}}
+
+# {{{ sub Load 
+
+=head2 Load ID
+  
+  Loads a watcher by the primary key of the watchers table ($Watcher->id)
+  
+=cut
+
+sub Load  {
+    my $self = shift;
+    my $identifier = shift;
+    
+    if ($identifier !~ /\D/) {
+       $self->SUPER::LoadById($identifier);
+    }
+    else {
+       return (0, "That's not a numerical id");
+    }
+}
+
+# }}}
+
+# {{{ sub LoadByValue
+
+=head2 LoadByValue PARAMHASH
+  
+LoadByValue takes a parameter hash with the following attributes:
+
+  Email, Owner, Scope, Type, Value
+
+The same rules enforced at create are enforced by Load.
+
+Returns a tuple of (retval, msg). Retval is 1 on success and 0 on failure.
+msg describes what happened in a human readable form.
+
+=cut
+
+sub LoadByValue {
+    my $self = shift;
+    my %args = ( Email => undef, 
+                Owner => undef,
+                Scope => undef,
+                Type => undef,
+                Value => undef,
+                @_);
+    
+    #TODO: all this code is being copied from Create. that\'s silly
+    
+    #Do we have someone this applies to?
+    unless (($args{'Owner'} =~ /^(\d*)$/) || ($args{'Email'} =~ /\@/)) {
+       return (0, "No user or email address specified");
+    }
+    
+    #if we only have an email address, try to resolve it to an owner
+    unless ($args{'Owner'}) {
+        my $User = new RT::User($RT::SystemUser);
+        $User->LoadByEmail($args{'Email'});
+        if ($User->id > 0) {
+            $args{'Owner'} = $User->id;
+           delete $args{'Email'};
+       }
+    }
+    
+    if ((defined ($args{'Type'})) and 
+       ($args{'Type'} !~ /^(Requestor|Cc|AdminCc)$/i)) {
+       return (0, "Invalid Type");
+    }
+    if ($args{'Owner'}) {
+       $self->LoadByCols( Type => $args{'Type'},
+                          Value => $args{'Value'},
+                          Owner => $args{'Owner'},
+                          Scope => $args{'Scope'},
+                        );
+    }
+    else {
+       $self->LoadByCols( Type => $args{'Type'},
+                          Email => $args{'Email'},
+                          Value => $args{'Value'},
+                          Scope => $args{'Scope'},
+                        );
+    }  
+    unless ($self->Id) {
+       return(0, "Couldn\'t find that watcher");
+    }
+    return (1, "Watcher loaded");
+}
+
+# }}}
+
+# {{{ sub OwnerObj 
+
+=head2 OwnerObj
+
+Return an RT Owner Object for this Watcher, if we have one
+
+=cut
+
+sub OwnerObj  {
+    my $self = shift;
+    if (!defined $self->{'OwnerObj'}) {
+       require RT::User;
+       $self->{'OwnerObj'} = RT::User->new($self->CurrentUser);
+       if ($self->Owner) {
+           $self->{'OwnerObj'}->Load($self->Owner);
+       } else {
+           return $RT::Nobody->UserObj;
+       }
+    }
+    return ($self->{'OwnerObj'});
+}
+# }}}
+
+# {{{ sub Email
+
+=head2 Email
+
+This custom data accessor does the right thing and returns
+the 'Email' attribute of this Watcher object. If that's undefined,
+it returns the 'EmailAddress' attribute of its 'Owner' object, which is
+an RT::User object.
+
+=cut
+
+sub Email {
+    my $self = shift;
+    
+    # IF Email is defined, return that. Otherwise, return the Owner's email address
+    if (defined($self->__Value('Email'))) {
+       return ($self->__Value('Email'));
+    }
+    elsif ($self->Owner) {
+       return ($self->OwnerObj->EmailAddress);
+    }
+    else {
+       return ("Data error");
+    }
+}
+# }}}
+  
+# {{{ sub IsUser
+
+=head2 IsUser
+
+Returns true if this watcher object is tied to a user object. (IE it
+isn't sending to some other email address).
+Otherwise, returns undef
+
+=cut
+
+sub IsUser {
+    my $self = shift;
+    # if this watcher has an email address glued onto it,
+    # return undef
+
+    if (defined($self->__Value('Email'))) {
+        return undef;
+    }
+    else {
+        return 1;
+    }
+}
+
+# }}}
+
+# {{{ sub _Accessible 
+sub _Accessible  {
+  my $self = shift;
+  my %Cols = (
+             Email => 'read/write',
+             Scope => 'read/write',
+             Value => 'read/write',
+             Type => 'read/write',
+             Quiet => 'read/write',
+             Owner => 'read/write',          
+             Creator => 'read/auto',
+             Created => 'read/auto',
+             LastUpdatedBy => 'read/auto',
+             LastUpdated => 'read/auto'
+            );
+  return($self->SUPER::_Accessible(@_, %Cols));
+}
+# }}}
+
+1;
diff --git a/rt/lib/RT/Watchers.pm b/rt/lib/RT/Watchers.pm
new file mode 100755 (executable)
index 0000000..c55adda
--- /dev/null
@@ -0,0 +1,226 @@
+# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Attic/Watchers.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
+# (c) 1996-2000 Jesse Vincent <jesse@fsck.com>
+# This software is redistributable under the terms of the GNU GPL
+
+=head1 NAME
+
+  RT::Watchers - Collection of RT Watcher objects
+
+=head1 SYNOPSIS
+
+  use RT::Watchers;
+  my $watchers = new RT::Watchers($CurrentUser);
+  while (my $watcher = $watchers->Next()) {
+    print $watcher->Id . "is a watcher";
+  }  
+
+=head1 DESCRIPTION
+
+This module should never be called directly by client code. it's an internal module which
+should only be accessed through exported APIs in Ticket, Queue and other similar objects.
+
+
+=head1 METHODS
+
+=begin testing
+
+ok(require RT::TestHarness);
+ok(require RT::Watchers);
+
+=end testing
+
+=cut
+
+package RT::Watchers;
+
+use strict;
+use vars qw( @ISA );
+
+
+require RT::EasySearch;
+require RT::Watcher;
+@ISA= qw(RT::EasySearch);
+
+
+# {{{ sub _Init
+sub _Init  {
+  my $self = shift;
+  
+  $self->{'table'} = "Watchers";
+  $self->{'primary_key'} = "id";
+  return($self->SUPER::_Init(@_));
+}
+# }}}
+
+# {{{ sub Limit 
+
+=head2 Limit
+
+  A wrapper around RT::EasySearch::Limit which sets
+the default entry aggregator to 'AND'
+
+=cut
+
+sub Limit  {
+  my $self = shift;
+  my %args = ( ENTRYAGGREGATOR => 'AND',
+              @_);
+
+  $self->SUPER::Limit(%args);
+}
+# }}}
+
+# {{{ sub LimitToTicket
+
+=head2 LimitToTicket
+
+Takes a single arg which is a ticket id
+Limits to watchers of that ticket
+
+=cut
+
+sub LimitToTicket { 
+  my $self = shift;
+  my $ticket = shift;
+  $self->Limit( ENTRYAGGREGATOR => 'OR',
+               FIELD => 'Value',
+               VALUE => $ticket);
+  $self->Limit (ENTRYAGGREGATOR => 'AND',
+               FIELD => 'Scope',
+               VALUE => 'Ticket');
+}
+# }}}
+
+# {{{ sub LimitToQueue 
+
+=head2 LimitToQueue
+
+Takes a single arg, which is a queue id
+Limits to watchers of that queue.
+
+=cut
+
+sub LimitToQueue  {
+  my $self = shift;
+  my $queue = shift;
+  $self->Limit (ENTRYAGGREGATOR => 'OR',
+               FIELD => 'Value',
+               VALUE => $queue);
+  $self->Limit (ENTRYAGGREGATOR => 'AND',
+               FIELD => 'Scope',
+               VALUE => 'Queue');
+}
+# }}}
+
+# {{{ sub LimitToType 
+
+=head2 LimitToType
+
+Takes a single string as its argument. That string is a watcher type
+which is one of 'Requestor', 'Cc' or 'AdminCc'
+Limits to watchers of that type
+
+=cut
+
+
+sub LimitToType  {
+  my $self = shift;
+  my $type = shift;
+  $self->Limit(FIELD => 'Type',
+              VALUE => "$type");
+}
+# }}}
+
+# {{{ sub LimitToRequestors 
+
+=head2 LimitToRequestors
+
+Limits to watchers of type 'Requestor'
+
+=cut
+
+sub LimitToRequestors  {
+  my $self = shift;
+  $self->LimitToType("Requestor");
+}
+# }}}
+
+# {{{ sub LimitToCc 
+
+=head2 LimitToCc
+
+Limits to watchers of type 'Cc'
+
+=cut
+
+sub LimitToCc  {
+    my $self = shift;
+    $self->LimitToType("Cc");
+}
+# }}}
+
+# {{{ sub LimitToAdminCc 
+
+=head2 LimitToAdminCc
+
+Limits to watchers of type AdminCc
+
+=cut
+
+sub LimitToAdminCc  {
+    my $self = shift;
+    $self->LimitToType("AdminCc");
+}
+# }}}
+
+# {{{ sub Emails 
+
+=head2 Emails
+
+# Return a (reference to a) list of emails
+
+=cut
+
+sub Emails  {
+    my $self = shift;
+    my @list;    # List is a list of watcher email addresses
+
+    # $watcher is an RT::Watcher object
+    while (my $watcher=$self->Next()) {
+       push(@list, $watcher->Email);
+    }
+    return \@list;
+}
+# }}}
+
+# {{{ sub EmailsAsString
+
+=head2 EmailsAsString
+
+# Returns the RT::Watchers->Emails as a comma seperated string
+
+=cut
+
+sub EmailsAsString {
+    my $self = shift;
+    return(join(", ",@{$self->Emails}));
+}
+# }}}
+
+# {{{ sub NewItem 
+
+
+
+sub NewItem  {
+    my $self = shift;
+    
+    use RT::Watcher;
+    my  $item = new RT::Watcher($self->CurrentUser);
+    return($item);
+}
+# }}}
+1;
+
+
+
+
diff --git a/rt/lib/test.pl b/rt/lib/test.pl
new file mode 100644 (file)
index 0000000..f0da5df
--- /dev/null
@@ -0,0 +1,52 @@
+# Before `make install' is performed this script should be runnable with
+# `make test'. After `make install' it should work as `perl test.pl'
+
+######################### We start with some black magic to print on failure.
+
+# Change 1..1 below to 1..last_test_to_print .
+# (It may become useful if the test is moved to ./t subdirectory.)
+
+BEGIN { $| = 1; print "1..1\n"; }
+END {print "not ok 1\n" unless $loaded;}
+use RT;
+$loaded = 1;
+print "ok 1\n";
+
+######################### End of black magic.
+
+# Insert your test code below (better if it prints "ok 13"
+# (correspondingly "not ok 13") depending on the success of chunk 13
+# of the test code):
+
+use RT::Record;
+use RT::EasySearch;
+use RT::Handle;
+use RT::Ticket;
+use RT::Tickets;
+use RT::ACE;
+use RT::ACL;
+use RT::Watcher;
+use RT::Watchers;
+use RT::Scrip;
+use RT::Scrips;
+use RT::ScripAction;
+use RT::ScripCondition;
+use RT::ScripActions;
+use RT::ScripConditions;
+use RT::Transaction;
+use RT::Transactions;
+use RT::Group;
+use RT::GroupMembers;
+use RT::User;
+use RT::Users;
+use RT::CurrentUser;
+use RT::Attachment;
+use RT::Attachments;
+use RT::Keyword;
+use RT::Keywords;
+use RT::KeywordSelect;
+use RT::KeywordSelects;
+use RT::ObjectKeyword;
+use RT::ObjectKeywords;
+use RT::Date;
+
diff --git a/rt/tools/cpan2rpm b/rt/tools/cpan2rpm
new file mode 100644 (file)
index 0000000..9b54cb9
--- /dev/null
@@ -0,0 +1,299 @@
+#!/usr/bin/perl -w
+
+
+# Take a perl tarball and make a specfile for it. Now with bundles.
+#
+# Copyright 2000,2001 Simon Wilkinson. All rights reserved.
+#
+# 10/18/2001 - <jesse@bestpractical.com>
+#              Added resolution of prereq_pm modules
+#
+# This program is free software; you can redistribute it 
+# and/or modify it under the same terms as Perl itself.
+#
+
+use strict;
+
+use CPAN;
+use POSIX;
+use Sys::Hostname;
+use File::Basename;
+use Getopt::Long;
+
+use vars qw ($DEBUG $ARCH $builddir $seen @report);
+
+$ARCH = 'i386';
+
+$DEBUG = 0;
+# Icky globals
+
+my $release;
+my $package;
+
+$seen = {};
+
+
+sub usage 
+{
+  print STDERR <<EOM;
+
+Usage: cpan2rpm [--release <release>] [--builddir <rpm build dir>]  <Perl::Module>
+
+Where:
+<release> is the release number of the RPM you wish to produce
+<Perl::Module> is the name of the module to build
+EOM
+  exit(1);
+}
+
+
+my $ret=GetOptions("release" => \$release,
+                  "builddir=s" => \$builddir);
+
+$package=$ARGV[0];
+usage() if !$package;
+$release=1 if !$release;
+
+
+$builddir=ExtractRpmMacro($ENV{HOME}."/.rpmmacros","_topdir") if !$builddir;
+$builddir=ExtractRpmMacro("/etc/rpm/macros","_topdir") if !$builddir;
+$builddir=getcwd() if !$builddir;
+  
+die "Build directory $builddir doesn't look like an RPM build root\n"
+    if ((! -d "$builddir/SPECS") || (! -d "$builddir/SOURCES"));
+
+process($package,$release);
+
+print join("\n",@report)."\n";
+
+sub process {
+  my ($infile,$release)=@_;
+
+  
+
+# Convert our installation list into an unbundled one
+  unbundle($infile);
+
+  print "Building $infile\n";
+
+    cpan2rpm($infile,$builddir,$release);
+
+}
+
+# Given a Module, try to split it into its required components - this
+# currently only handles Bundles, but could also be extended to deal with
+# prereqs as well.
+
+sub unbundle {
+  my ($item) = @_;
+
+  if ($item=~/Bundle::/) {
+    my $obj=CPAN::Shell->expand('Bundle',$item);
+
+    foreach my $kid ($obj->contains) {
+       process($kid,$release);
+    }
+  }
+}
+
+
+sub cpan2rpm($$$) {
+  my ($infile,$builddir,$release) = @_;
+
+  my $ret;
+
+  my $obj=CPAN::Shell->expand('Module',$infile);
+
+  print "CPAN tells us the following about $infile:\n",$obj->as_string if ($DEBUG);
+
+  $ret=fetch_source($obj,$builddir);
+  $ret=build_specfile($obj,$builddir,$release) if !$ret;
+  
+  return $ret;
+}
+
+# FIXME: Some error handling in the function below wouldn't go amiss ...
+sub fetch_source {
+  my ($obj,$builddir)=@_;
+
+  # Minor Sanity checks
+  my $id=$obj->{ID};
+
+  return "Error: No file for $id\n" 
+     if $obj->cpan_file eq "N/A";
+  return "Error: $id says 'Contact Author'\n" 
+     if $obj->cpan_file =~ /^Contact Author/;
+  return "Error: $id is contained within Perl itself!\n"
+     if ($obj->cpan_file =~/perl-5\.\d?\.\d?\.tar\.gz/xo);
+
+  # We do this so we can take advantage of CPAN's object caching. This is
+  # pinched from the CPAN::Distribution::get method, which we can't use
+  # directly, as it untars the package as well - which we let RPM do.
+  my $dist = $CPAN::META->instance('CPAN::Distribution',$obj->cpan_file);
+
+
+  my($local_wanted) =
+       MM->catfile($CPAN::Config->{keep_source_where},
+                   "authors",
+                   "id",
+                   split("/",$dist->{ID})
+                   );
+
+  my $local_file = CPAN::FTP->localize("authors/id/$dist->{ID}", $local_wanted);
+  
+  $dist->{localfile} = $local_file;
+
+  $dist->verifyMD5 if ($CPAN::META->has_inst('MD5'));
+
+
+  # Find all the prereqs for this distribution, then build em.
+  # TODO this should be somewhere else
+
+  $dist->make;
+  build_prereqs( $dist->prereq_pm());
+
+
+
+  my $infile=basename($obj->cpan_file);
+
+  File::Copy::copy($local_file,"$builddir/SOURCES/$infile");
+
+  return undef;
+}
+
+
+sub build_prereqs($) {
+  my ($prereqs) = @_;
+  
+  foreach my $prereq (keys %{$prereqs}) {
+       process ($prereq, $release);
+  }
+}
+sub build_specfile($$$) {
+  my ($obj,$builddir,$release) = @_;
+
+  my $source=basename($obj->cpan_file);
+
+  # don't go through dependencies on something we've already dealt with
+  return() if ($seen->{$source});
+  $seen->{$source} = 1; 
+
+my ($name,$version)=($source=~/(.*)-(.*)\.tar\.gz/);
+  return "Couldn't get a name for $source\n" if !$name;
+  return "Couldn't get a version for $source\n" if !$version; 
+  
+  my $summary="$name module for perl";
+  my $description=$obj->{description};
+  $description= $summary if !$description;
+
+  open(SPEC, ">$builddir/SPECS/perl-$name.spec")
+    or die "Couldn't open perl-$name.spec for writing.";
+  print SPEC <<EOF;
+
+Summary: $summary
+Name: perl-$name
+Version: $version
+Release: $release
+Copyright: distributable
+Group: Applications/CPAN
+Source0: $source
+Url: http://www.cpan.org
+BuildRoot: /var/tmp/perl-${name}-buildroot/
+Requires: perl 
+
+%description
+This is a perl module, autogenerated by cpan2rpm. The original package's
+description was:
+
+$description
+
+%prep
+%setup -q -n $name-%{version}
+
+%build
+CFLAGS="\$RPM_OPT_FLAGS" perl Makefile.PL
+make
+
+%clean
+rm -rf \$RPM_BUILD_ROOT
+
+%install
+rm -rf \$RPM_BUILD_ROOT
+eval `perl '-V:installarchlib'`
+mkdir -p \$RPM_BUILD_ROOT/\$installarchlib
+make PREFIX=\$RPM_BUILD_ROOT/usr install
+/usr/lib/rpm/brp-compress
+find \$RPM_BUILD_ROOT/usr -type f -print | sed "s\@^\$RPM_BUILD_ROOT\@\@g" | grep -v perllocal.pod > $name-$version-filelist
+
+%files -f ${name}-${version}-filelist
+%defattr(-,root,root)
+
+%changelog
+EOF
+  print SPEC "* ",POSIX::strftime("%a %b %d %Y",localtime()), " ",$ENV{USER}," <",$ENV{USER},"\@",hostname(),">\n";
+  print SPEC "- Spec file automatically generated by cpan2rpm\n";
+
+  close(SPEC);
+
+  system("rpm -ba $builddir/SPECS/perl-$name.spec >/dev/null") == 0
+    or push (@report,  "RPM of $source failed with : $!\n"); 
+  system("rpm -Uvh $builddir/RPMS/$ARCH/perl-$name-$version-$release.$ARCH.rpm") == 0
+    or warn "RPM of $source could not be installed: $!\n";
+
+  push (@report,  "Built perl-$name-$version-$release.$ARCH.rpm");
+}
+
+sub ExtractRpmMacro {
+  my ($file,$macro) = @_;
+
+  my $handle=new IO::File;
+
+  if (!$handle->open($file)) {
+    return undef;
+  }
+
+  while(<$handle>) {
+    if (/\%$macro (.*)/) {
+       return $1;
+    }
+  }
+
+  return undef;
+}
+
+=head1 NAME
+
+cpan2rpm - fetch and convert CPAN packages to RPMs
+
+=head1 SYNOPSIS
+
+cpan2rpm --release <release> <package>
+
+=head1 DESCRIPTION
+
+cpan2rpm provides a quick way of creating RPM packages from perl modules
+published on CPAN. It interfaces with the perl CPAN module to fetch the
+file from the selected mirror, and then creates a spec file from the 
+information in CPAN, and invokes RPM on that spec file.
+
+Files are created in the users RPM build root.
+
+=head1 OPTIONS
+
+=over 4
+
+=item release
+
+Sets the release number of the created RPMs.
+
+=back
+
+=head1 SEE ALSO
+
+rpm(1)
+
+=head1 AUTHOR
+
+Simon Wilkinson <sxw@sxw.org.uk>
diff --git a/rt/tools/initdb b/rt/tools/initdb
new file mode 100644 (file)
index 0000000..ffb1ae3
--- /dev/null
@@ -0,0 +1,216 @@
+#!/usr/bin/perl -w
+# $Header: /home/cvs/cvsroot/freeside/rt/tools/Attic/initdb,v 1.1 2002-08-12 06:17:08 ivan Exp $
+
+use strict;
+use vars qw($PROMPT $SCHEMA_FILE $SCHEMA_DIR
+           $ACTION $DEBUG $DB_TYPE $DB_HOME 
+           $DB_HOST $DB_PORT $DB_DBA $DB_DATABASE $DB_DBA_PASSWORD);
+
+use DBI;
+use DBIx::DataSource qw( create_database drop_database );
+
+
+$|=1; #unbuffer that output.
+
+$DEBUG=0;
+$PROMPT = 1; #by default, should at least *ask* before nuking databases
+$SCHEMA_DIR ="etc";
+$SCHEMA_FILE = "$SCHEMA_DIR/schema.pm"; #hmm
+
+($DB_TYPE, $DB_HOME, $DB_HOST, $DB_PORT, $DB_DBA, $DB_DATABASE, $ACTION) = @ARGV;
+
+
+if ($DEBUG) {
+  print_config_params();
+}
+my $dsn = "dbi:$DB_TYPE:";
+
+if (($DB_TYPE eq 'Pg') or ($DB_TYPE eq 'mysql')) {
+   $dsn .= "dbname=$DB_DATABASE";
+   if ($DB_HOST) {
+       $dsn .= ";host=$DB_HOST";
+    }
+   if ($DB_PORT) {
+       $dsn .= ";port=$DB_PORT";
+   }
+}
+elsif ($DB_TYPE eq 'Oracle') {
+   $dsn .= "$DB_DATABASE";
+}
+
+
+if ($ACTION eq 'create') {
+    unless ($DB_TYPE eq 'Oracle') {
+        print "Now creating a database for RT.\n";
+       prompt_for_dba_password();
+       create_db();
+    }
+}
+elsif ($ACTION eq 'drop' ) {
+    unless ($DB_TYPE eq 'Oracle') {
+       print "Now dropping the RT2 database.\n";
+        prompt_for_dba_password();
+        drop_db();
+    }
+}
+elsif ($ACTION eq 'insert' ) {
+    print "Now populating database schema.\n";
+    prompt_for_dba_password();
+    insert_schema();
+}
+elsif ($ACTION eq 'generate') {
+    prompt_for_dba_password();
+    generate_schema();
+}
+else {
+    print STDERR '$ACTION unspecified. Makefile error. It was '.$ACTION ;
+    exit(-1);
+}
+
+
+# {{{ sub prompt_for_dba_password
+
+sub prompt_for_dba_password {
+    print "Enter the $DB_TYPE password for $DB_DBA: ";
+
+    system "stty -echo";
+    $DB_DBA_PASSWORD = scalar(<STDIN>); #keep off commandline
+    system "stty echo";
+    chomp $DB_DBA_PASSWORD;
+
+}
+# }}}
+
+# {{{ sub print_config_params
+sub print_config_params {
+    print <<END;
+Database creation parameters:
+
+DB_TYPE         = $DB_TYPE
+DB_HOME         = $DB_HOME
+DB_HOST         = $DB_HOST
+DB_DBA          = $DB_DBA
+DB_DBA_PASSWORD = <hidden>
+DB_DATABASE     = $DB_DATABASE
+END
+}
+# }}}
+
+# {{{ sub drop_db
+sub drop_db {
+    
+    if ( $PROMPT ) {
+       print <<END;
+
+About to drop $DB_TYPE database $DB_DATABASE.
+WARNING: This will erase all data in $DB_DATABASE.
+If you have an existing RT 2.x installation, this will destroy all your data.
+i
+END
+       exit unless _yesno();
+       
+    }
+    
+
+  print "\nDropping $DB_TYPE database $DB_DATABASE.\n";
+  drop_database( $dsn, $DB_DBA, $DB_DBA_PASSWORD )
+    or warn $DBIx::DataSource::errstr;
+
+
+}
+# }}}
+
+# {{{ sub generate_schema
+sub generate_schema {
+    my @schema = generate_schema_from_hash();
+    print "Generating schema for $DB_TYPE...";
+    
+    system('mv', "$SCHEMA_DIR/schema.$DB_TYPE", "$SCHEMA_DIR/schema.$DB_TYPE.bak")
+      if (-f "$SCHEMA_DIR/schema.$DB_TYPE");
+    open(SCHEMA, ">$SCHEMA_DIR/schema.$DB_TYPE");
+    foreach my $line (@schema) {
+       print SCHEMA "$line;\n";
+    }
+    close(SCHEMA);
+    print "done.\n";
+}
+# }}}
+
+# {{{ sub insert_schema
+sub insert_schema {
+    my (@schema);
+    print "\nCreating database schema.\n";
+   
+    my $dbh = DBI->connect( $dsn, $DB_DBA, $DB_DBA_PASSWORD ) or die $DBI::errstr;    
+    
+    if ( -f "$SCHEMA_DIR/schema.$DB_TYPE") {
+       open (SCHEMA, "<$SCHEMA_DIR/schema.$DB_TYPE");
+       my $statement = "";
+       foreach my $line (<SCHEMA>) {
+           $statement .= $line;
+           if ($line =~ /;$/) {
+               $statement =~ s/;$//g;
+               push @schema, $statement;
+               $statement = "";
+           }
+       }       
+    }  
+    
+    else {
+        @schema = generate_schema_from_hash();
+     }
+    
+    foreach my $statement (@schema) {
+       print STDERR $statement if $DEBUG;
+       my $sth = $dbh->prepare($statement) or die $dbh->errstr;
+       unless ($sth->execute) {
+           print STDERR "Problem with statement:\n $statement\n";
+           die $sth->errstr;
+       }
+    }
+    
+    
+    $dbh->disconnect;
+    print "schema sucessfully inserted\n";
+    
+}
+# }}}
+
+# {{{ sub generate_schema_from_hash
+sub generate_schema_from_hash {
+    my (@schema);
+    
+    require DBIx::DBSchema;    
+    my $schema_href = do "$SCHEMA_FILE" or die $@ || $!;
+    my $schema = DBIx::DBSchema->pretty_read($schema_href);
+    
+    
+    foreach my $statement ( $schema->sql($dsn, $DB_DBA, $DB_DBA_PASSWORD ) ) {
+       print STDERR $statement if $DEBUG;
+       chomp $statement;
+       push @schema, $statement;
+       
+    }
+    return (@schema);
+    
+}
+# }}}
+
+# {{{ sub create_db
+sub create_db {
+    
+    print "\nCreating $DB_TYPE database $DB_DATABASE.\n";
+    create_database( $dsn, $DB_DBA, $DB_DBA_PASSWORD )
+      or die $DBIx::DataSource::errstr;
+
+}
+# }}}
+
+# {{{ sub _yesno
+sub _yesno {
+    print "Proceed [y/N]:";
+    my $x = scalar(<STDIN>);
+    $x =~ /^y/i;
+}
+
+# }}}
diff --git a/rt/tools/insertdata b/rt/tools/insertdata
new file mode 100755 (executable)
index 0000000..b3e76e6
--- /dev/null
@@ -0,0 +1,618 @@
+#!/usr/bin/perl -w
+#
+# $Header: /home/cvs/cvsroot/freeside/rt/tools/Attic/insertdata,v 1.1 2002-08-12 06:17:08 ivan Exp $
+# RT is (c) 1996-2002 Jesse Vincent (jesse@bestpractical.com);
+
+package RT;
+use strict;
+use vars qw($VERSION $Handle $Nobody $SystemUser $item);
+
+use lib "!!RT_LIB_PATH!!";
+use lib "!!RT_ETC_PATH!!";
+
+#This drags in  RT's config.pm
+use config;
+use Carp;
+
+use RT::Handle;
+use RT::User;
+use RT::CurrentUser;
+
+# 
+my $LastVersion = shift || undef;
+my $LastMinorVersion = undef;
+
+#connect to the db
+$RT::Handle = new RT::Handle($RT::DatabaseType);
+$RT::Handle->Connect();
+
+#Put together a current user object so we can create a User object
+my $CurrentUser = new RT::CurrentUser();
+
+if ($LastVersion) {
+    if ( $LastVersion =~ /^2.0.(\d+)$/ ) {
+        $LastMinorVersion = $1;
+        print "Looking for new objects to add to the database"
+          . " since $LastVersion\n\n";
+    }
+    else {
+        print "This tool does not support upgrades from development releases "
+          . "or non 2.0.x versions";
+    }
+}
+else {    # this is a virgin install
+    print "Checking for existing system user...";
+    my $test_user = RT::User->new($CurrentUser);
+    $test_user->Load('RT_System');
+    if ( $test_user->id ) {
+        print "Found!\n\nYou appear to have already run insertdata.\n"
+          . "Exiting, so as not to clobber your existing data. To ERASE your\n"
+          . "RT database and start over, type 'make dropdb; make install' in\n"
+          . "the RT installation directory. If you just meant to upgrade the\n"
+          . "content of your database, rerun this program as: \n",
+          "       $0 <version>\n"
+          . "where <version> is the last RELEASED version of RT you installed\n"
+          . "for example, if you're upgrading from 2.0.4, you'd type:\n"
+          . "       $0 2.0.4\n";
+        exit(-1);
+
+    }
+    else {
+        print "not found.  This appears to be a new installation";
+    }
+
+    print "Creating system user...";
+    my $RT_System = new RT::User($CurrentUser);
+
+    my ( $val, $msg ) = $RT_System->_BootstrapCreate(
+        Name     => 'RT_System',
+        RealName => 'The RT System itself',
+        Comments =>
+'Do not delete or modify this user. It is integral to RT\'s internal database structures',
+        Privileged => '2',
+        Creator    => '1'
+    );
+
+    if ($val) {
+        print "done.\n";
+    }
+    else {
+        print "$msg\n";
+        exit(1);
+    }
+
+}
+
+#now that we bootstrapped that little bit, we can use the standard RT cli
+# helpers  to do what we need
+
+use RT::Interface::CLI qw(CleanEnv LoadConfig DBConnect
+  GetCurrentUser GetMessageContent);
+
+#Clean out all the nasties from the environment
+CleanEnv();
+
+#Load etc/config.pm and drop privs
+LoadConfig();
+
+#Connect to the database and get RT::SystemUser and RT::Nobody loaded
+DBConnect();
+
+$CurrentUser->LoadByName('RT_System');
+
+# {{{ Users
+
+my @users;
+
+unless ($LastVersion) {
+    @users = (
+        {
+            Name     => 'Nobody',
+            RealName => 'Nobody in particular',
+            Comments => 'Do not delete or modify this user. It is integral '
+              . 'to RT\'s internal data structures',
+            Privileged => '2',
+        },
+
+        {
+            Name         => 'root',
+            Gecos        => 'root',
+            RealName     => 'Enoch Root',
+            Password     => 'password',
+            EmailAddress => "root\@localhost",
+            Comments     => 'SuperUser',
+            Privileged   => '1',
+        }
+    );
+}
+
+# }}}
+
+# {{{ Groups 
+
+my @groups;
+unless ($LastVersion) {
+    @groups = (
+        {
+            Name        => 'Everyone',
+            Description => 'Pseudogroup for internal use',
+            Pseudo      => '1',
+        },
+        {
+            Name        => 'Owner',
+            Description => 'Pseudogroup for internal use',
+            Pseudo      => '1',
+        },
+        {
+            Name        => 'Requestor',
+            Description => 'Pseudogroup for internal use',
+            Pseudo      => '1',
+        },
+        {
+            Name        => 'Cc',
+            Description => 'Pseudogroup for internal use',
+            Pseudo      => '1',
+        },
+        {
+            Name        => 'AdminCc',
+            Description => 'Pseudogroup for internal use',
+            Pseudo      => '1',
+        },
+    );
+}
+
+# }}}
+
+# {{{ ACL
+my @acl;
+
+unless ($LastVersion) {
+    @acl = (    #TODO: make this actually take the serial # granted to root.
+        {
+            PrincipalId    => '1',
+            PrincipalType  => 'User',
+            RightName      => 'SuperUser',
+            RightScope     => 'System',
+            RightAppliesTo => '0'
+        },
+        {
+            PrincipalId    => '2',
+            PrincipalType  => 'User',
+            RightName      => 'SuperUser',
+            RightScope     => 'System',
+            RightAppliesTo => '0'
+        },
+
+        {
+            PrincipalId    => '3',
+            PrincipalType  => 'User',
+            RightName      => 'SuperUser',
+            RightScope     => 'System',
+            RightAppliesTo => '0'
+        }
+
+    );
+}
+
+# }}}
+
+# {{{ Queues
+
+my @queues;
+unless ($LastVersion) {
+    @queues = (
+        {
+            Name              => 'general',
+            Description       => 'The default queue',
+            CorrespondAddress => "rt\@localhost",
+            CommentAddress    => "rt-comment\@localhost"
+        }
+    );
+}
+
+# }}}
+
+# {{{ ScripActions
+
+my @ScripActions;
+
+unless ($LastVersion) {
+    @ScripActions = (
+
+        {
+            Name        => 'AutoreplyToRequestors',
+            Description =>
+'Always sends a message to the requestors independent of message sender',
+            ExecModule => 'Autoreply',
+            Argument   => 'Requestor'
+        },
+        {
+            Name        => 'NotifyRequestors',
+            Description => 'Sends a message to the requestors',
+            ExecModule  => 'Notify',
+            Argument    => 'Requestor'
+        },
+        {
+            Name        => 'NotifyOwnerAsComment',
+            Description => 'Sends mail to the owner',
+            ExecModule  => 'NotifyAsComment',
+            Argument    => 'Owner'
+        },
+        {
+            Name        => 'NotifyOwner',
+            Description => 'Sends mail to the owner',
+            ExecModule  => 'Notify',
+            Argument    => 'Owner'
+        },
+        {
+            Name        => 'NotifyAdminCcsAsComment',
+            Description => 'Sends mail to the administrative Ccs as a comment',
+            ExecModule  => 'NotifyAsComment',
+            Argument    => 'AdminCc'
+        },
+        {
+            Name        => 'NotifyAdminCcs',
+            Description => 'Sends mail to the administrative Ccs',
+            ExecModule  => 'Notify',
+            Argument    => 'AdminCc'
+        },
+
+        {
+            Name        => 'NotifyRequestorsAndCcsAsComment',
+            Description => 'Send mail to requestors and Ccs as a comment',
+            ExecModule  => 'NotifyAsComment',
+            Argument    => 'Requestor,Cc'
+        },
+
+        {
+            Name        => 'NotifyRequestorsAndCcs',
+            Description => 'Send mail to requestors and Ccs',
+            ExecModule  => 'Notify',
+            Argument    => 'Requestor,Cc'
+        },
+
+        {
+            Name        => 'NotifyAllWatchersAsComment',
+            Description => 'Send mail to all watchers',
+            ExecModule  => 'NotifyAsComment',
+            Argument    => 'All'
+        },
+        {
+            Name        => 'NotifyAllWatchers',
+            Description => 'Send mail to all watchers',
+            ExecModule  => 'Notify',
+            Argument    => 'All'
+        },
+    );
+}
+
+if ( $LastMinorVersion < 12 ) {
+    push (
+        @ScripActions,
+        {
+            Name        => 'NotifyOtherRecipientsAsComment',
+            Description => 'Sends mail to explicitly listed Ccs and Bccs',
+            ExecModule  => 'NotifyAsComment',
+            Argument    => 'OtherRecipients'
+        },
+        {
+            Name        => 'NotifyOtherRecipients',
+            Description => 'Sends mail to explicitly listed Ccs and Bccs',
+            ExecModule  => 'Notify',
+            Argument    => 'OtherRecipients'
+        },
+    );
+}
+
+# }}}
+
+# {{{ ScripConditions
+
+my @ScripConditions;
+unless ($LastVersion) {
+    @ScripConditions = (
+        {
+            Name                 => 'OnCreate',
+            Description          => 'When a ticket is created',
+            ApplicableTransTypes => 'Create',
+            ExecModule           => 'AnyTransaction',
+        },
+
+        {
+            Name                 => 'OnTransaction',
+            Description          => 'When anything happens',
+            ApplicableTransTypes => 'Any',
+            ExecModule           => 'AnyTransaction',
+        },
+        {
+
+            Name                 => 'OnCorrespond',
+            Description          => 'Whenever correspondence comes in',
+            ApplicableTransTypes => 'Correspond',
+            ExecModule           => 'AnyTransaction',
+        },
+
+        {
+
+            Name                 => 'OnComment',
+            Description          => 'Whenever comments come in',
+            ApplicableTransTypes => 'Comment',
+            ExecModule           => 'AnyTransaction'
+        },
+        {
+
+            Name                 => 'OnStatus',
+            Description          => 'Whenever a ticket\'s status changes',
+            ApplicableTransTypes => 'Status',
+            ExecModule           => 'AnyTransaction',
+
+        },
+        {
+            Name                 => 'OnResolve',
+            Description          => 'Whenever a ticket is resolved.',
+            ApplicableTransTypes => 'Status',
+            ExecModule           => 'StatusChange',
+            Argument             => 'resolved'
+
+        },
+
+    );
+}
+
+# }}}
+
+# {{{ Templates
+my @templates;
+
+unless ($LastVersion) {
+    @templates = (
+        {
+            Queue       => '0',
+            Name        => 'Autoreply',
+            Description => 'Default Autoresponse Template',
+            Content     => 'Subject: AutoReply: {$Ticket->Subject}
+
+
+Greetings,
+
+This message has been automatically generated in response to the
+creation of a trouble ticket regarding:
+       "{$Ticket->Subject()}", 
+a summary of which appears below.
+
+There is no need to reply to this message right now.  Your ticket has been
+assigned an ID of [{$rtname} #{$Ticket->id()}].
+
+Please include the string:
+
+         [{$rtname} #{$Ticket->id}]
+
+in the subject line of all future correspondence about this issue. To do so, 
+you may reply to this message.
+
+                        Thank you,
+                        {$Ticket->QueueObj->CorrespondAddress()}
+
+-------------------------------------------------------------------------
+{$Transaction->Content()}
+'
+        },
+
+        {
+
+            #                  id => 2,
+            Queue       => '0',
+            Name        => 'Transaction',
+            Description => 'Default transaction template',
+            Content     => '
+
+
+{$Transaction->CreatedAsString}: Request {$Ticket->id} was acted upon.
+Transaction: {$Transaction->Description}
+       Queue: {$Ticket->QueueObj->Name}
+     Subject: {$Transaction->Subject || $Ticket->Subject || "(No subject given)"}
+       Owner: {$Ticket->OwnerObj->Name}
+  Requestors: {$Ticket->Requestors->EmailsAsString()}
+      Status: {$Ticket->Status}
+ Ticket <URL: {$RT::WebURL}Ticket/Display.html?id={$Ticket->id} >
+-------------------------------------------------------------------------
+{$Transaction->Content()}'
+        },
+
+        {
+
+            Queue       => '0',
+            Name        => 'AdminCorrespondence',
+            Description => 'Default admin correspondence template',
+            Content     => '
+
+
+<URL: {$RT::WebURL}Ticket/Display.html?id={$Ticket->id} >
+
+{$Transaction->Content()}'
+        },
+
+        {
+            Queue       => '0',
+            Name        => 'Correspondence',
+            Description => 'Default correspondence template',
+            Content     => '
+
+{$Transaction->Content()}'
+        },
+
+        {
+            Queue       => '0',
+            Name        => 'AdminComment',
+            Description => 'Default admin comment template',
+            Content     =>
+'Subject: [Comment] {my $s=($Transaction->Subject||$Ticket->Subject); $s =~ s/\\[Comment\\]//g; $comment =~ s/^Re//i; $s;}
+
+
+{$RT::WebURL}Ticket/Display.html?id={$Ticket->id}
+This is a comment.  It is not sent to the Requestor(s):
+
+{$Transaction->Content()}
+'
+        },
+
+        {
+            Queue       => '0',
+            Name        => 'StatusChange',
+            Description => 'Ticket status changed',
+            Content     => 'Subject: Status Changed to: {$Transaction->NewValue}
+
+
+{$RT::WebURL}Ticket/Display.html?id={$Ticket->id}
+
+{$Transaction->Content()}
+'
+        },
+
+        {
+
+            Queue       => '0',
+            Name        => 'Resolved',
+            Description => 'Ticket Resolved',
+            Content     => 'Subject: Ticket Resolved
+
+According to our records, your request has been resolved. If you have any
+further questions or concerns, please respond to this message.
+'
+        }
+    );
+}
+
+# }}}
+
+# {{{ Scrips;
+
+my @scrips;
+unless ($LastVersion) {
+     @scrips = (
+                { Queue => 0,
+                  ScripCondition => 'OnCreate',
+                  ScripAction => 'AutoreplyToRequestors',
+                  Template => 'Autoreply'
+                },
+                { Queue => 0,
+                  ScripCondition => 'OnCreate',
+                  ScripAction => 'NotifyAdminCcs',
+                  Template => 'Transaction',
+                },
+                { Queue => 0,
+                  ScripCondition => 'OnCorrespond',
+                  ScripAction => 'NotifyAllWatchers',
+                  Template => 'Correspondence',
+                },
+                { Queue => 0,
+                  ScripCondition => 'OnComment',
+                  ScripAction => 'NotifyAdminCcsAsComment',
+                  Template => 'AdminComment',
+                },
+     ) 
+}
+if ( $LastMinorVersion < 12 ) {
+    push (
+        @scrips,
+                { Queue => 0,
+                  ScripCondition => 'OnComment',
+                  ScripAction => 'NotifyOtherRecipientsAsComment',
+                  Template => 'Correspondence',
+                },
+                { Queue => 0,
+                  ScripCondition => 'OnCorrespond',
+                  ScripAction => 'NotifyOtherRecipients',
+                  Template => 'Correspondence',
+                },
+        );
+}
+# }}}
+
+print "Creating ACL...";
+use RT::ACE;
+for $item (@acl) {
+    my $new_entry = new RT::ACE($CurrentUser);
+
+    #Using an internal function. this should never be used outside of the bootstrap script
+    my $return = $new_entry->_BootstrapRight(%$item);
+    print $return. ".";
+}
+print "done.\n";
+
+print "Creating users...";
+use RT::User;
+foreach $item (@users) {
+    my $new_entry = new RT::User($CurrentUser);
+    my ( $return, $msg ) = $new_entry->Create(%$item);
+    print "(Error: $msg)" unless ($return);
+    print $return. ".";
+}
+print "done.\n";
+
+print "Creating groups...";
+use RT::Group;
+foreach $item (@groups) {
+    my $new_entry = new RT::Group($CurrentUser);
+    my $return    = $new_entry->Create(%$item);
+    print $return. ".";
+}
+print "done.\n";
+
+print "Creating queues...";
+use RT::Queue;
+for $item (@queues) {
+    my $new_entry = new RT::Queue($CurrentUser);
+    my ( $return, $msg ) = $new_entry->Create(%$item);
+    print "(Error: $msg)" unless ($return);
+    print $return. ".";
+}
+
+print "done.\n";
+print "Creating ScripActions...";
+
+use RT::ScripAction;
+for $item (@ScripActions) {
+    my $new_entry = new RT::ScripAction($CurrentUser);
+    my $return    = $new_entry->Create(%$item);
+    print $return. ".";
+}
+
+print "done.\n";
+print "Creating ScripConditions...";
+
+use RT::ScripCondition;
+for $item (@ScripConditions) {
+    my $new_entry = new RT::ScripCondition($CurrentUser);
+    my $return    = $new_entry->Create(%$item);
+    print $return. ".";
+}
+
+print "done.\n";
+
+print "Creating templates...";
+
+use RT::Template;
+for $item (@templates) {
+    my $new_entry = new RT::Template($CurrentUser);
+    my $return    = $new_entry->Create(%$item);
+    print $return. ".";
+}
+print "done.\n";
+
+print "Creating Scrips...";
+
+use RT::Scrip;
+for $item (@scrips) {
+        my $new_entry = RT::Scrip->new($CurrentUser);
+        my ($return,$msg) = $new_entry->Create(%$item);
+        print "(Error: $msg)" unless ($return);
+        print $return.".";
+
+}
+print "done.\n";
+
+$RT::Handle->Disconnect();
+
+1;
+
diff --git a/rt/tools/testdeps b/rt/tools/testdeps
new file mode 100644 (file)
index 0000000..ddc3381
--- /dev/null
@@ -0,0 +1,115 @@
+#!/usr/bin/perl -w
+
+# $Header: /home/cvs/cvsroot/freeside/rt/tools/Attic/testdeps,v 1.1 2002-08-12 06:17:08 ivan Exp $
+
+# Copyright 2000 Jesse Vincent <jesse@fsck.com>
+# Distributed under the GNU General Public License
+# 
+
+#
+# This is just a basic script that checks to make sure that all
+# the modules needed by RT before you can install it.
+#
+
+use strict;
+
+use vars qw($mode $dbd $module @modules);
+
+$mode = shift || print_help();
+$dbd = shift || print_help();
+
+@modules = qw(
+Digest::MD5
+Storable
+DBI 1.18
+DBIx::DataSource 0.02
+DBIx::SearchBuilder 0.48
+HTML::Entities 
+MLDBM
+Net::Domain
+Net::SMTP
+Params::Validate 0.02
+HTML::Mason 1.02
+CGI::Cookie 1.20
+Apache::Cookie
+Apache::Session 1.53
+Date::Parse
+Date::Format 
+MIME::Entity 5.108
+Mail::Mailer 1.20
+Getopt::Long 2.24
+Tie::IxHash 
+Text::Wrapper 
+Text::Template
+File::Spec 0.8
+Errno
+FreezeThaw
+File::Temp 
+Log::Dispatch 1.6
+);
+
+
+if ($dbd =~ /mysql/i) {
+       push @modules, ('DBD::mysql','2.0416'); 
+}
+elsif ($dbd =~ /oracle/i) {
+       push @modules, ('DBD::Oracle','');
+}
+elsif ($dbd =~ /pg/i) {
+       push @modules, ('DBD::Pg','');
+}
+use CPAN;
+
+while ($module= shift @modules) {
+       my $version = "";
+       $version = " ". shift (@modules) . " " if ($modules[0] =~ /^([\d\.]*)$/);
+       print "Checking for $module$version";
+       eval "use $module$version" ;
+       if ($@) {
+       &resolve_dependency($module, $version) 
+       }
+       else {
+       print "...found\n";
+       }
+}
+
+sub print_help {
+print <<EOF;
+
+$0 FLAG DBTYPE
+
+
+$0 is a tool for RT that will tell you if you've got all
+the modules RT depends on properly installed.
+
+Flags: (only one flag is valid for a given run)
+
+-quiet will check to see if we've got everything we need
+       and will exit with a return code of (1) if we don't.
+
+-warn will tell you what isn't properly installed
+
+-fix will use CPAN to magically make everything better
+
+DBTYPE is one of:
+       oracle, pg, mysql
+
+EOF
+
+exit(0);
+}
+
+sub resolve_dependency {
+       my $module = shift;
+       my $version = shift;
+        print "....$module$version not installed.";
+    if ($mode =~ /-f/) {
+       $module = "DBD::mysql::Install" if ($module =~ /DBD::mysql/);
+       
+        print "Installing with CPAN...";
+        CPAN::install($module);
+     }
+     print "\n";
+       exit(1) if ($mode =~ /-q/);
+}      
+       
diff --git a/rt/webrt/Admin/Elements/CreateQueueCalled b/rt/webrt/Admin/Elements/CreateQueueCalled
new file mode 100755 (executable)
index 0000000..aeed6e7
--- /dev/null
@@ -0,0 +1,3 @@
+<FORM METHOD=get ACTION="<% $RT::WebPath %>/Admin/Queues/Create.html">
+Create a queue called <INPUT NAME="Name" size=10><input type=submit>
+</form>
diff --git a/rt/webrt/Admin/Elements/CreateUserCalled b/rt/webrt/Admin/Elements/CreateUserCalled
new file mode 100755 (executable)
index 0000000..7e4bb75
--- /dev/null
@@ -0,0 +1,3 @@
+<FORM METHOD=get ACTION="<%$RT::WebPath%>/Admin/Users/Create.html">
+New user called <INPUT NAME="Name" size=10><input type=submit value="Create">
+</form>
diff --git a/rt/webrt/Admin/Elements/EditUserComments b/rt/webrt/Admin/Elements/EditUserComments
new file mode 100755 (executable)
index 0000000..1ac7e18
--- /dev/null
@@ -0,0 +1,9 @@
+<& /Elements/Header, Title => "Comments about $name" &>
+These comments aren't generally visible to the user:<br>
+<input type="hidden" name="id" value="<%$id%>">
+<TEXTAREA COLS=60 ROWS=15 WRAP=SOFT NAME="Comments"><% $UserObj->Comments %></TEXTAREA>
+</FORM>
+
+<%ARGS>
+$UserObj => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/GrantQueueRightsTo b/rt/webrt/Admin/Elements/GrantQueueRightsTo
new file mode 100755 (executable)
index 0000000..3850a18
--- /dev/null
@@ -0,0 +1,30 @@
+<BR>
+
+% if ($msg) {
+<%$msg%>
+% } elsif ($Users) {
+<ul>
+% while (my $u = $Users->Next ) {
+<li> <%$u->Name%> (<%$u->RealName%>) <& SelectQueueRights, Name => "GrantTo".$u->id &>
+% }
+</ul>
+% }
+
+<%INIT>
+my ($msg, $Users);
+if (!$ARGS{'UserString'}) {
+$msg = "No users selected.";
+ }
+else {
+    $Users = new RT::Users($session{'CurrentUser'});
+    $Users->Limit(FIELD => $ARGS{'UserField'},
+                 VALUE => $ARGS{'UserString'},
+                 OPERATOR => $ARGS{'UserOp'});
+     }
+</%INIT>
+
+<%ARGS>
+$UserField => 'Name'
+$UserOp => '='
+$UserString => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/GroupTabs b/rt/webrt/Admin/Elements/GroupTabs
new file mode 100755 (executable)
index 0000000..261bef1
--- /dev/null
@@ -0,0 +1,18 @@
+<& /Admin/Elements/Tabs, subtabs => $subtabs, current_tab => 'Admin/Groups/' &>
+<hr>
+<%INIT>
+my $subtabs = { 
+             Basics => { title => 'Basics',
+                         path => "Admin/Groups/Modify.html?id=". $GroupObj->id
+                       }
+       };
+
+unless ($GroupObj->Pseudo) {
+$subtabs->{'Members'} = { title => 'Members',
+                         path => "Admin/Groups/Members.html?id=".$GroupObj->id };
+}
+</%INIT>
+
+<%ARGS>
+$GroupObj => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/Header b/rt/webrt/Admin/Elements/Header
new file mode 100755 (executable)
index 0000000..95acdac
--- /dev/null
@@ -0,0 +1,5 @@
+<& /Elements/Header, Title => $Title &>
+
+<%ARGS>
+$Title => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/ListGlobalKeywordSelects b/rt/webrt/Admin/Elements/ListGlobalKeywordSelects
new file mode 100644 (file)
index 0000000..b24d689
--- /dev/null
@@ -0,0 +1,15 @@
+% while (my $KeywordSelect = $KeywordSelects->Next()) {
+
+<%$KeywordSelect->Name %>:
+<% $KeywordSelect->Single ? 'Single' : 'Multiple' %>
+children of
+<% $KeywordSelect->KeywordObj->Path %>
+% if ($KeywordSelect->Depth) {
+         up to <%$KeywordSelect->Depth%> levels deep
+% }
+<BR>
+%} 
+<%INIT>
+my $KeywordSelects = RT::KeywordSelects->new($session{'CurrentUser'});
+$KeywordSelects->LimitToGlobals();
+</%INIT>
diff --git a/rt/webrt/Admin/Elements/ListGlobalScrips b/rt/webrt/Admin/Elements/ListGlobalScrips
new file mode 100755 (executable)
index 0000000..2f044bf
--- /dev/null
@@ -0,0 +1,10 @@
+%  while (my $scrip = $Scrips->Next ) {
+<% $scrip->ConditionObj->Name %> 
+<% $scrip->ActionObj->Name %> 
+with template <% $scrip->TemplateObj->Name %>
+<BR>
+%   }
+<%init>
+my $Scrips = new RT::Scrips ($session{'CurrentUser'});
+$Scrips->LimitToGlobal();
+</%INIT>
diff --git a/rt/webrt/Admin/Elements/ModifyKeyword b/rt/webrt/Admin/Elements/ModifyKeyword
new file mode 100644 (file)
index 0000000..4b01c36
--- /dev/null
@@ -0,0 +1,95 @@
+<FORM METHOD="get" ACTION="<%$RT::WebPath%>/Admin/Keywords/Modify.html">
+[<%$title |n %>]<BR>
+
+<INPUT TYPE="hidden" NAME="id" VALUE="<% $id %>">
+Keyword <INPUT NAME="Name" VALUE="<% $Keyword->Name %>"><BR>
+
+Parent <SELECT NAME="Parent">
+             <OPTION VALUE=""<% defined($Keyword->Parent) ? '' : ' SELECTED' %>>-</OPTION>
+%while ( $parent = $parents->Next ) {
+             <OPTION VALUE="<% $parent->id %>"<% defined($Keyword->Parent) && $parent->id == $Keyword->Parent ? ' SELECTED' : '' %>><% $parent->Name %></OPTION>
+%}
+</SELECT>
+
+
+Kids <FONT SIZE="-2">(separate by
+<INPUT TYPE="radio" NAME="delim" VALUE="n"<% $delim eq 'n' ? ' CHECKED' : '' %>>
+line or
+<INPUT TYPE="radio" NAME="delim" VALUE="s"<% $delim eq 's' ? ' CHECKED' : '' %>>
+whitespace)</FONT><BR>
+
+<TEXTAREA NAME="Kids" ROWS=4><% $kidstring %></TEXTAREA>
+<BR>
+
+<& /Elements/Submit, Label => $submit &>
+</FORM>
+
+<%INIT>
+
+my $Keyword = new RT::Keyword($session{CurrentUser});
+my ($title, $submit, %kids, $kid);
+
+if ( $Create ) {
+    $title = "Create a new Keyword";
+    $submit = "Create";
+    $id = "new";
+    %kids = ();
+    $Parent = ''; #silence 
+} elsif ( $id eq 'new' ) {
+    $id = $Keyword->Create( Name => $Name, Parent => $Parent )
+      or Abort("can't create keyword Name=>$Name, Parent=>$Parent");
+} else {
+    $Keyword->Load($id) || Abort("Can't load keyword id $id");
+    #foreach my $field ( grep eval "defined(\$$_)", qw( Name Parent )) {
+    #  eval "\$Keyword->Set(\$field=>\$$field); #sigh
+    #}
+    $Keyword->SetName($Name) if defined($Name);
+    $Keyword->SetParent($Parent) if defined($Parent);
+}
+
+$title = "Modify the Keyword <B>". $Keyword->Name. "</B>";
+$submit = "Modify";
+
+my $kids = new RT::Keywords($session{CurrentUser});
+$kids->Limit( FIELD => 'Parent', VALUE => $id, OPERATOR => '=' );
+$kids{$kid->Name} = $kid while $kid = $kids->Next;
+
+if ( defined($Kids) ) {
+    my %newkids;
+    if ( $delim eq 'n' ) {
+       %newkids = map { $_=>1 } split(/\n/, $Kids);
+    } elsif ( $delim eq 's' ) {
+       %newkids = map { $_=>1 } split(' ', $Kids);
+    } else {
+       Abort("'$delim' isn't a valid keyword delimiter.");
+    }
+    foreach ( grep { ! defined($newkids{$_}) } keys %kids ) {
+       $kids{$_}->Delete;
+       delete $kids{$_};
+    }
+    foreach ( grep { ! defined($kids{$_}) } keys %newkids ) {
+       $kids{$_} = new RT::Keyword($session{CurrentUser});
+       $kids{$_}->Create( Name => $_, Parent => $id )
+         or Abort("can't create keyword Name=>$_, Parent=>$id");
+    }
+
+}
+
+
+my $parent;
+my $parents = new RT::Keywords($session{CurrentUser});
+$parents->UnLimit;
+
+$delim = ( grep /\s/, keys %kids ) ? 'n' : 's';
+my $kidstring = join("\n", keys %kids);
+
+</%INIT>
+
+<%ARGS>
+$id => undef
+$Create => undef
+$Name => undef
+$Parent => undef
+$Kids => undef
+$delim => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/ModifyKeywordSelect b/rt/webrt/Admin/Elements/ModifyKeywordSelect
new file mode 100644 (file)
index 0000000..470e629
--- /dev/null
@@ -0,0 +1,120 @@
+  <FORM NAME="ModifyKeywordSelect" METHOD=POST ACTION="<%$RT::WebPath%>/Admin/KeywordSelects/Modify.html">
+
+    [<%$title |n %>]
+    <BR>
+      
+      <INPUT TYPE="hidden" NAME="id" VALUE="<% $id %>">
+       Keyword 
+       <SELECT NAME="Parent">
+         
+%while ( $parent = $parents->Next ) {
+         
+         <OPTION VALUE="<% $parent->id %>" <% defined($KeywordSelect->Parent) && $parent->id == $KeywordSelect->Parent ? ' SELECTED' : '' %>><% $parent->Name %></OPTION>
+         
+% }
+         
+       </SELECT>
+       <BR>
+         Object 
+         <SELECT NAME="ObjectType">
+           <OPTION SELECTED>Ticket</OPTION>
+         </SELECT>
+         <BR>
+           
+<SCRIPT>
+function addOption(text, value, defaultselected, selected) {
+  var option = new Option(text, value, defaultselected, selected )
+  var length = document.ModifyKeywordSelect.ObjectValue.length;
+  document.ModifyKeywordSelect.ObjectValue.options[length] = option
+}
+function ChangeObjectValue(what) {
+  Value = what.options[what.selectedIndex].value
+  if ( Value == "(none)" ) {
+    document.ModifyKeywordSelect.ObjectValue.options.length = 0
+    addOption("(n/a)", "", false, false)
+  }
+  if ( Value == "Queue" ) {
+    document.ModifyKeywordSelect.ObjectValue.options.length = 0
+%foreach $queue ( keys %queues ) {
+    addOption("<% $queues{$queue} %>", "<% $queue %>", false, <% $queue == $KeywordSelect->ObjectValue ? 'true' : 'false' %> )
+%}
+  }
+}
+</SCRIPT>
+           
+           Limit to <SELECT NAME="ObjectField" onChange="ChangeObjectValue(this)">
+             <OPTION VALUE="" <% $KeywordSelect->ObjectField ? '' : ' SELECTED' %>>(none)</OPTION>
+             <OPTION VALUE="Queue" <% $KeywordSelect->ObjectField eq 'Queue' ? ' SELECTED' : '' %>>Queue</OPTION>
+           </SELECT> 
+           <SELECT NAME="ObjectValue">
+             <OPTION VALUE="<% $KeywordSelect->ObjectValue %>">
+               <% $KeywordSelect->ObjectField ? $queues{$KeywordSelect->ObjectValue} : "(n/a)" %></OPTION>
+           </SELECT><BR>
+             <INPUT TYPE="hidden" NAME="SingleMagic" VALUE="1">
+               <INPUT TYPE="checkbox" NAME="Single" VALUE="1" <% $KeywordSelect->Single ? ' CHECKED' : '' %>>Allow single selection only<BR>
+                   Limit to <INPUT TYPE="text" NAME="Generations" SIZE="2" VALUE="<% $KeywordSelect->Generations %>"> generations (0 = no limit)<BR>
+                       <& /Elements/Submit, Label => $submit &>
+
+</FORM>
+
+<%INIT>
+
+
+my $KeywordSelect = new RT::KeywordSelect($session{CurrentUser});
+  
+my($title, $submit);
+  
+if ( $Create ) {
+      $title = "Create a new KeywordSelect";
+      $submit = "Create";
+      $id = "new";
+} else {
+    if  ( $id eq 'new' ) {
+       $id = $KeywordSelect->Create (
+                                     Parent      => $Parent,
+                                     ObjectType  => $ObjectType,
+                                     ObjectField => $ObjectField,
+                                     ObjectValue => $ObjectValue,
+                                     Single      => $Single,
+                                     Generations => $Generations,
+                                    ) or Abort "can't create KeywordSelect";
+    } else {
+       $KeywordSelect->Load($id) || Abort("Can't load keyword id $id");
+       #false laziness
+       $KeywordSelect->SetParent($Parent) if defined($Parent);
+       $KeywordSelect->SetObjectType($ObjectType) if defined($ObjectType);
+       $KeywordSelect->SetObjectField($ObjectField) if defined($ObjectField);
+       $KeywordSelect->SetObjectValue($ObjectValue) if defined($ObjectValue);
+       $KeywordSelect->SetSingle($Single) if defined($SingleMagic);
+       $KeywordSelect->SetGenerations($Generations) if defined($Generations);
+    }
+    $title = "Modify the KeywordSelect <B>". $KeywordSelect->KeywordObj->Name. "</B>";
+    $submit = "Modify";
+    
+}
+  
+  my $parents = new RT::Keywords($session{CurrentUser});
+  $parents->UnLimit;
+  my $parent;
+
+my $queues = new RT::Queues($session{CurrentUser});
+$queues->UnLimit;
+
+my %queues;
+my $queue;
+$queues{$queue->id} = $queue->Name while $queue = $queues->Next;
+
+</%INIT>
+
+<%ARGS>
+$id => undef
+$Create => undef
+$Parent => undef
+$ObjectType => undef
+$ObjectField => undef
+$ObjectValue => undef
+$Single => undef
+$SingleMagic => undef
+$Generations => undef
+</%ARGS>
+
diff --git a/rt/webrt/Admin/Elements/ModifyQueue b/rt/webrt/Admin/Elements/ModifyQueue
new file mode 100755 (executable)
index 0000000..a641c81
--- /dev/null
@@ -0,0 +1,56 @@
+
+<& /Elements/TitleBoxStart, title => 'Editing Configuration for queue '.$QueueObj->Id &>
+
+<FORM ACTION="<%$RT::WebPath%>/Admin/Queues/Modify.html" METHOD=POST>
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$QueueObj->Id%>">
+<TABLE>
+<TR><TD ALIGN=RIGHT>
+Queue Name: 
+</TD>
+<TD><INPUT name="Name" value="<%$QueueObj->Name%>"></TD>
+</TR><TR>
+<TD ALIGN=RIGHT>
+Description:</TD><TD COLSPAN=3><INPUT name="Description" value="<%$QueueObj->Description%>" size=60></TD></TR>
+<TR>
+<TD ALIGN=RIGHT>
+Correspondence Address:
+</TD><TD>
+<INPUT name="CorrespondAddress" value="<%$QueueObj->CorrespondAddress%>">
+</TD>
+<TD ALIGN=RIGHT>
+
+Comment Address: </TD><TD>
+<INPUT NAME="CommentAddress" value="<%$QueueObj->CommentAddress%>">
+</TD>
+</TR><TR>
+
+<TD ALIGN=RIGHT>
+Priority starts at: 
+</TD><TD><INPUT NAME="InitialPriority" value="<%$QueueObj->InitialPriority %>">
+</TD>
+<TD ALIGN=RIGHT>
+Over time, priority moves toward:
+</TD><TD><INPUT NAME="FinalPriority" value="<%$QueueObj->FinalPriority %>">
+</TD>
+</TR>
+<TR>
+<TD ALIGN=RIGHT>
+Requests should be due in:
+</TD><TD>
+<INPUT NAME="DefaultDueIn" VALUE="<%$QueueObj->DefaultDueIn%>"> days.
+</TD>
+</TR>
+</TABLE>
+<& /Elements/Submit &>
+</form>
+<& /Elements/TitleBoxEnd &>
+
+<%INIT>
+
+</%INIT>
+
+<%ARGS>
+
+
+$QueueObj => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/ModifyTemplate b/rt/webrt/Admin/Elements/ModifyTemplate
new file mode 100755 (executable)
index 0000000..6e4f8a3
--- /dev/null
@@ -0,0 +1,78 @@
+
+<& /Elements/TitleBoxStart, title => 'Editing Configuration for user '.$UserObj->Name &>
+
+<FORM ACTION="<%$RT::WebPath%>/Admin/ModifyUser.html" METHOD=POST>
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$UserObj->Id%>">
+
+Name: <input name="Name" value="<%$UserObj->Name%>">
+<BR>
+New Password: <input type=password name="Pass1"><BR>
+Retype Password: <input type=password name="Pass2"><BR>
+
+Comments: <TEXTAREA name="Comments" COLS=20 ROWS=5>
+<%$UserObj->Comments%></TEXTAREA>
+
+<BR>
+Signature: <TEXTAREA COLS=80 ROWS=5 name="Signature">
+<%$UserObj->Signature%>"></TEXTAREA>
+<BR>
+EmailAddress: <input name="EmailAddress" value="<%$UserObj->EmailAddress%>">
+<BR>
+FreeformContactInfo: <input name="FreeformContactInfo" value="<%$UserObj->FreeformContactInfo%>">
+<BR>
+Organization: <input name="Organization" value="<%$UserObj->Organization%>">
+<BR>
+RealName: <input name="RealName" value="<%$UserObj->RealName%>">
+<BR>
+NickName: <input name="NickName" value="<%$UserObj->NickName%>">
+<BR>
+Lang: <input name="Lang" value="<%$UserObj->Lang%>">
+<BR>
+EmailEncoding: <input name="EmailEncoding" value="<%$UserObj->EmailEncoding%>">
+<BR>
+WebEncoding: <input name="WebEncoding" value="<%$UserObj->WebEncoding%>">
+<BR>
+ExternalContactInfoId: <input name="ExternalContactInfoId" value="<%$UserObj->ExternalContactInfoId%>">
+<BR>
+ContactInfoSystem: <input name="ContactInfoSystem" value="<%$UserObj->ContactInfoSystem%>">
+<BR>
+Gecos: <input name="Gecos" value="<%$UserObj->Gecos%>">
+<BR>
+ExternalAuthId: <input name="ExternalAuthId" value="<%$UserObj->ExternalAuthId%>">
+<BR>
+AuthSystem: <input name="AuthSystem" value="<%$UserObj->AuthSystem%>">
+<BR>
+HomePhone: <input name="HomePhone" value="<%$UserObj->HomePhone%>">
+<BR>
+WorkPhone: <input name="WorkPhone" value="<%$UserObj->WorkPhone%>">
+<BR>
+MobilePhone: <input name="MobilePhone" value="<%$UserObj->MobilePhone%>">
+<BR>
+PagerPhone: <input name="PagerPhone" value="<%$UserObj->PagerPhone%>">
+<BR>
+Address1: <input name="Address1" value="<%$UserObj->Address1%>">
+<BR>
+Address2: <input name="Address2" value="<%$UserObj->Address2%>">
+<BR>
+City: <input name="City" value="<%$UserObj->City%>">
+<BR>
+State: <input name="State" value="<%$UserObj->State%>">
+<BR>
+Zip: <input name="Zip" value="<%$UserObj->Zip%>">
+<BR>
+Country: <input name="Country" value="<%$UserObj->Country%>">
+<BR>
+
+<input type=submit>
+</form>
+<& /Elements/TitleBoxEnd &>
+
+<%INIT>
+
+</%INIT>
+
+<%ARGS>
+
+
+$UserObj => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/ModifyUser b/rt/webrt/Admin/Elements/ModifyUser
new file mode 100755 (executable)
index 0000000..53aa027
--- /dev/null
@@ -0,0 +1,77 @@
+
+<& /Elements/TitleBoxStart, title => 'Editing Configuration for user '.$UserObj->Name &>
+
+<FORM ACTION="<%$RT::WebPath%>/Admin/Users/Modify.html" METHOD=POST>
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$UserObj->Id%>">
+
+Name: <input name="Name" value="<%$UserObj->Name%>">
+<BR>
+New Password: <input type=password name="Pass1"><BR>
+Retype Password: <input type=password name="Pass2"><BR>
+
+Comments: <TEXTAREA name="Comments" COLS=80 ROWS=5 WRAP=VIRTUAL>
+<%$UserObj->Comments%></TEXTAREA>
+
+<BR>
+Signature: <TEXTAREA COLS=80 ROWS=5 name="Signature" WRAP=HARD>
+<%$UserObj->Signature%></TEXTAREA>
+<BR>
+EmailAddress: <input name="EmailAddress" value="<%$UserObj->EmailAddress%>">
+<BR>
+FreeformContactInfo: <input name="FreeformContactInfo" value="<%$UserObj->FreeformContactInfo%>">
+<BR>
+Organization: <input name="Organization" value="<%$UserObj->Organization%>">
+<BR>
+RealName: <input name="RealName" value="<%$UserObj->RealName%>">
+<BR>
+NickName: <input name="NickName" value="<%$UserObj->NickName%>">
+<BR>
+Lang: <input name="Lang" value="<%$UserObj->Lang%>">
+<BR>
+EmailEncoding: <input name="EmailEncoding" value="<%$UserObj->EmailEncoding%>">
+<BR>
+WebEncoding: <input name="WebEncoding" value="<%$UserObj->WebEncoding%>">
+<BR>
+ExternalContactInfoId: <input name="ExternalContactInfoId" value="<%$UserObj->ExternalContactInfoId%>">
+<BR>
+ContactInfoSystem: <input name="ContactInfoSystem" value="<%$UserObj->ContactInfoSystem%>">
+<BR>
+Gecos: <input name="Gecos" value="<%$UserObj->Gecos%>">
+<BR>
+ExternalAuthId: <input name="ExternalAuthId" value="<%$UserObj->ExternalAuthId%>">
+<BR>
+AuthSystem: <input name="AuthSystem" value="<%$UserObj->AuthSystem%>">
+<BR>
+HomePhone: <input name="HomePhone" value="<%$UserObj->HomePhone%>">
+<BR>
+WorkPhone: <input name="WorkPhone" value="<%$UserObj->WorkPhone%>">
+<BR>
+MobilePhone: <input name="MobilePhone" value="<%$UserObj->MobilePhone%>">
+<BR>
+PagerPhone: <input name="PagerPhone" value="<%$UserObj->PagerPhone%>">
+<BR>
+Address1: <input name="Address1" value="<%$UserObj->Address1%>">
+<BR>
+Address2: <input name="Address2" value="<%$UserObj->Address2%>">
+<BR>
+City: <input name="City" value="<%$UserObj->City%>">
+<BR>
+State: <input name="State" value="<%$UserObj->State%>">
+<BR>
+Zip: <input name="Zip" value="<%$UserObj->Zip%>">
+<BR>
+Country: <input name="Country" value="<%$UserObj->Country%>">
+<BR>
+<& /Elements/Submit &>
+</form>
+<& /Elements/TitleBoxEnd &>
+
+<%INIT>
+
+</%INIT>
+
+<%ARGS>
+
+
+$UserObj => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/QueueRightsForUser b/rt/webrt/Admin/Elements/QueueRightsForUser
new file mode 100644 (file)
index 0000000..e62a124
--- /dev/null
@@ -0,0 +1,17 @@
+<UL>
+%while(my $ACE = $ACL->Next) {
+
+<LI><checkbox name="delete_ace_<%$ACE->id%>"> <%$ACE->RightName%> (<%$ACE->UserObj->RealName%>)
+
+%}
+</UL>
+
+<%INIT>
+my $ACL = new RT::ACL($session{'CurrentUser'});
+$ACL->LimitToQueue($QueueObj->id);
+$ACL->LimitPrincipalToUser($PrincipalId);
+</%INIT>
+<%ARGS>
+$PrincipalId => undef
+$QueueObj => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/QueueTabs b/rt/webrt/Admin/Elements/QueueTabs
new file mode 100755 (executable)
index 0000000..b7da7e0
--- /dev/null
@@ -0,0 +1,36 @@
+<& /Admin/Elements/Tabs, subtabs => $subtabs, current_tab => 'Admin/Queues/' &>
+<hr>
+<%INIT>
+  my $subtabs = {
+                A => { title => 'Basics',
+                       path => "Admin/Queues/Modify.html?id=".$id,
+                          },
+                B => { title => 'Watchers',
+                       path => "Admin/Queues/People.html?id=".$id,
+                     },
+
+                C => { title => 'Scrips',
+                            path => "Admin/Queues/Scrips.html?id=".$id,
+                          },
+                D => { title => 'Templates',
+                               path => "Admin/Queues/Templates.html?id=".$id,
+                             },
+                E => { title => 'Keyword Selections',
+                               path => "Admin/Queues/Keywords.html?id=".$id,
+                       },
+                F => { title => 'Group Rights',
+                         path => "Admin/Queues/GroupRights.html?id=".$id,
+                       },      
+                G => { title => 'User Rights',
+                         path => "Admin/Queues/UserRights.html?id=".$id,
+                       },
+
+
+                
+};
+</%INIT>
+
+  
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectKeywordSelect b/rt/webrt/Admin/Elements/SelectKeywordSelect
new file mode 100644 (file)
index 0000000..f5a8d77
--- /dev/null
@@ -0,0 +1,22 @@
+<input size=10 name="<%$NamePrefix%>-Name" value="<% $KeywordSelect->Name %>">:
+<& /Admin/Elements/SelectSingleOrMultiple, 
+         Name => $NamePrefix.'-Single',
+         Default => $KeywordSelect->Single &>
+         
+children of
+<& /Elements/SelectKeyword, Root => '0', 
+                           Name => $NamePrefix.'-Keyword',
+                            Default => $KeywordSelect->KeywordObj->Id &>
+         up to <input name="<%$NamePrefix%>-Depth" size=2 value="<%$KeywordSelect->Depth%>"> levels deep.
+<%INIT>
+unless ($NamePrefix) {
+       $NamePrefix = $KeywordSelect->Id;
+}
+$NamePrefix = "KeywordSelect-$NamePrefix";
+
+</%INIT>
+
+<%ARGS>
+$KeywordSelect => undef
+$NamePrefix => undef
+</%ARGS>
\ No newline at end of file
diff --git a/rt/webrt/Admin/Elements/SelectModifyGroup b/rt/webrt/Admin/Elements/SelectModifyGroup
new file mode 100644 (file)
index 0000000..45d437f
--- /dev/null
@@ -0,0 +1,10 @@
+%while ( $Group = $Groups->Next) {
+<A HREF="Modify.html?id=<%$Group->id%>"><%$Group->id%>: <%$Group->Name%></a><BR>
+%}
+<%INIT>
+my ($Group);
+my $Groups = new RT::Groups($session{'CurrentUser'});
+$Groups->UnLimit;
+</%INIT>
+<%ARGS>
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectModifyKeyword b/rt/webrt/Admin/Elements/SelectModifyKeyword
new file mode 100644 (file)
index 0000000..6af2232
--- /dev/null
@@ -0,0 +1,13 @@
+%while ( $keyword = $keywords->Next ) {
+<A HREF="/Admin/Keywords/Modify.html?id=<%$keyword->id%>"><%$keyword->id%>: <%$keyword->Name%></a><BR>
+%}
+
+<%INIT>
+
+use RT::Keywords;
+
+my $keyword;
+my $keywords = new RT::Keywords $session{CurrentUser};
+$keywords->UnLimit;
+</%INIT>
+
diff --git a/rt/webrt/Admin/Elements/SelectModifyKeywordSelect b/rt/webrt/Admin/Elements/SelectModifyKeywordSelect
new file mode 100644 (file)
index 0000000..c91eb6c
--- /dev/null
@@ -0,0 +1,13 @@
+%while ( $keywordselect = $keywordselects->Next ) {
+<A HREF="/Admin/KeywordSelects/Modify.html?id=<%$keywordselect->id%>"><%$keywordselect->id%>: ( <%$keywordselect->Parent%>: <%$keywordselect->KeywordObj->Name%> )</a><BR>
+%}
+
+<%INIT>
+
+use RT::KeywordSelects;
+
+my $keywordselect;
+my $keywordselects = new RT::KeywordSelects $session{CurrentUser};
+$keywordselects->UnLimit;
+</%INIT>
+
diff --git a/rt/webrt/Admin/Elements/SelectModifyQueue b/rt/webrt/Admin/Elements/SelectModifyQueue
new file mode 100755 (executable)
index 0000000..1c6cd7d
--- /dev/null
@@ -0,0 +1,10 @@
+%while ( $queue = $queues->Next) {
+<A HREF="Modify.html?id=<%$queue->id%>"><%$queue->id%>: <%$queue->Name%></a><BR>
+%}
+<%INIT>
+my ($queue);
+my $queues = new RT::Queues($session{'CurrentUser'});
+$queues->UnLimit;
+</%INIT>
+<%ARGS>
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectModifyUser b/rt/webrt/Admin/Elements/SelectModifyUser
new file mode 100755 (executable)
index 0000000..da49212
--- /dev/null
@@ -0,0 +1,26 @@
+%while ( $user = $users->Next) {
+<A HREF="Modify.html?id=<%$user->id%>"><%$user->id%>: <%$user->Name%></a><BR>
+%}
+<%INIT>
+my ($user);
+my $users = new RT::Users($session{'CurrentUser'});
+$users->Limit(FIELD => 'id',
+              VALUE => $RT::SystemUser->id,
+              OPERATOR => '!=' );
+
+if (defined $IdLike) {
+$users->Limit(FIELD => 'Name',
+              VALUE => $IdLike,
+              OPERATOR => 'LIKE' );
+}
+if (defined $EmailLike) {
+$users->Limit(FIELD => 'EmailAddress',
+              VALUE => $EmailLike,
+              OPERATOR => 'LIKE');
+
+}
+</%INIT>
+<%ARGS>
+$IdLike => undef
+$EmailLike => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectQueueRights b/rt/webrt/Admin/Elements/SelectQueueRights
new file mode 100755 (executable)
index 0000000..6861d40
--- /dev/null
@@ -0,0 +1,29 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Admin/Elements/Attic/SelectQueueRights,v 1.1 2002-08-12 06:17:08 ivan Exp $
+
+<SELECT NAME ="<%$Name%>">
+<OPTION VALUE="">-</OPTION>
+%foreach $right (@rights) {
+<OPTION VALUE="<%$right%>" <%($Default eq $right) && 'SELECTED'%>><%$right%></OPTION>
+% }
+</SELECT>
+<%ONCE>
+
+use RT::ACE;
+my $ACE = new RT::ACE($session{'CurrentUser'});
+my %QueueRights = $ACE->QueueRights;
+my %TicketRights = $ACE->TicketRights;
+
+my ($key, $right, @rights);
+
+foreach $key (sort keys %QueueRights) {
+push (@rights, $QueueRights{$key} . " ($key)");
+}
+foreach $key (sort keys %TicketRights) {
+push (@rights, $TicketRights{$key} . " ($key)");
+}
+</%ONCE>
+<%ARGS>
+$Name => undef
+$Default => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectRights b/rt/webrt/Admin/Elements/SelectRights
new file mode 100644 (file)
index 0000000..0ac7749
--- /dev/null
@@ -0,0 +1,58 @@
+<INPUT TYPE=HIDDEN NAME="CheckACL"  VALUE="<%$ACLDesc%>">
+     <TABLE BORDER=0>
+<TR>
+<TD valign=top>
+<h3>New rights</h3> 
+<SELECT SIZE=5  MULTIPLE  NAME="GrantACE-<%$ACLDesc%>">
+% foreach $right (sort keys %Rights) {
+      <OPTION VALUE="<%$right%>"  
+       ><%$right%></OPTION>
+% }
+<OPTION VALUE="" SELECTED>(no value)</OPTION>
+</SELECT>
+</TD>
+<TD valign=top> 
+<h3>Current rights</h3>
+<i>(Check box to revoke right)</i> <BR>
+% while (my $right = $ACLObj->Next()) {
+% if ($right->RightName) {
+<input type=checkbox value="<%$right->Id%>" name="RevokeACE"> <%$right->RightName%><br>
+% }
+%  }
+</TD>
+</TR>
+</TABLE>
+<%INIT>
+    my ($right, $ACLDesc, $AppliesTo, %Rights);
+   
+       
+    my $ACLObj = new RT::ACL($session{'CurrentUser'});
+    my $ACE = new RT::ACE($session{'CurrentUser'});
+
+    if ($Scope eq 'Queue') { 
+        $AppliesTo = $QueueObj->Id;
+        $ACLObj->LimitToQueue($AppliesTo);
+        %Rights = $ACE->QueueRights();
+    } 
+    elsif ($Scope eq 'System') {
+        $AppliesTo = 0;
+        $ACLObj->LimitToSystem();
+        %Rights =  ( $ACE->SystemRights , $ACE->QueueRights());
+    }
+
+    if ($PrincipalType eq 'Group') {
+        $ACLObj->LimitPrincipalToGroup($PrincipalObj->Id);
+    } 
+    elsif ($PrincipalType eq 'User') {
+        $ACLObj->LimitPrincipalToUser($PrincipalObj->Id);
+    } 
+    
+    $ACLDesc = "$PrincipalType-".$PrincipalObj->Id."-$Scope-$AppliesTo";
+</%INIT>
+    
+<%ARGS>
+$PrincipalType => undef
+$PrincipalObj => undef
+$Scope => undef
+$QueueObj => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectScrip b/rt/webrt/Admin/Elements/SelectScrip
new file mode 100755 (executable)
index 0000000..4ae15d8
--- /dev/null
@@ -0,0 +1,25 @@
+<SELECT NAME=<%$Name%>>
+<OPTION VALUE="" 
+<% $Default eq undef && 'SELECTED' %>
+>-</OPTION>
+%while  (my $Scrip = $Scrips->Next) {
+<OPTION VALUE=<%$Scrip->Id%>
+<% $Scrip->Id == $Default && 'SELECTED' %>
+><%$Scrip->Name%>
+</OPTION>
+%}
+</SELECT>
+
+<%INIT>
+my $Scrips = RT::Scrips->new($session{'CurrentUser'});
+$Scrips->UnLimit;
+
+
+
+</%INIT>
+<%ARGS>
+
+$Default => undef
+$Name => 'Scrip'
+
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectScripAction b/rt/webrt/Admin/Elements/SelectScripAction
new file mode 100644 (file)
index 0000000..08a1734
--- /dev/null
@@ -0,0 +1,25 @@
+<SELECT NAME=<%$Name%>>
+<OPTION VALUE="" 
+<% $Default eq undef && 'SELECTED' %>
+>-</OPTION>
+%while  (my $ScripAction = $ScripActions->Next) {
+<OPTION VALUE=<%$ScripAction->Id%>
+<% $ScripAction->Id == $Default && 'SELECTED' %>
+><%$ScripAction->Name%>
+</OPTION>
+%}
+</SELECT>
+
+<%INIT>
+my $ScripActions = RT::ScripActions->new($session{'CurrentUser'});
+$ScripActions->UnLimit;
+
+
+
+</%INIT>
+<%ARGS>
+
+$Default => undef
+$Name => 'ScripAction'
+
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectScripCondition b/rt/webrt/Admin/Elements/SelectScripCondition
new file mode 100644 (file)
index 0000000..434f0c4
--- /dev/null
@@ -0,0 +1,25 @@
+<SELECT NAME=<%$Name%>>
+<OPTION VALUE="" 
+<% $Default eq undef && 'SELECTED' %>
+>-</OPTION>
+%while  (my $ScripCondition = $ScripConditions->Next) {
+<OPTION VALUE=<%$ScripCondition->Id%>
+<% $ScripCondition->Id == $Default && 'SELECTED' %>
+><%$ScripCondition->Name%>
+</OPTION>
+%}
+</SELECT>
+
+<%INIT>
+my $ScripConditions = RT::ScripConditions->new($session{'CurrentUser'});
+$ScripConditions->UnLimit;
+
+
+
+</%INIT>
+<%ARGS>
+
+$Default => undef
+$Name => 'ScripCondition'
+
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectSingleOrMultiple b/rt/webrt/Admin/Elements/SelectSingleOrMultiple
new file mode 100644 (file)
index 0000000..307b021
--- /dev/null
@@ -0,0 +1,20 @@
+  <select name="<%$Name%>">
+    <option value="1" <%$SingleDefault%>>Single</option>
+    <option value="0" <%$MultipleDefault%>>Multiple</option>
+  </select>    
+
+
+<%INIT>
+my ($SingleDefault, $MultipleDefault);
+if ($Default == 1) {
+    $SingleDefault = "SELECTED";
+}
+elsif ($Default == 0 ) {
+    $MultipleDefault = "SELECTED";
+}
+
+</%INIT>
+<%ARGS>
+$Name => 'Single'
+$Default => 1
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectTemplate b/rt/webrt/Admin/Elements/SelectTemplate
new file mode 100755 (executable)
index 0000000..76550dc
--- /dev/null
@@ -0,0 +1,37 @@
+<SELECT NAME=<%$Name%>>
+<OPTION VALUE="" 
+<% $Default eq 'none' && 'SELECTED' %>
+>-</OPTION>
+%while  (my $Template = $PrimaryTemplates->Next) {
+<OPTION VALUE=<%$Template->Id%>
+<% ($Template->Id == $Default) && 'SELECTED' %>
+><%$Template->Name%>
+</OPTION>
+%}
+%while  (my $Template = $OtherTemplates->Next) {
+<OPTION VALUE=<%$Template->Id%>
+<% ($Template->Id == $Default)  && 'SELECTED'%>
+>Global template: <%$Template->Name%>
+</OPTION>
+%}
+</SELECT>
+
+<%INIT>
+
+
+my $PrimaryTemplates = RT::Templates->new($session{'CurrentUser'});
+if ($DefaultQueue != 0) {
+$PrimaryTemplates->LimitToQueue($DefaultQueue);
+}
+
+my $OtherTemplates = RT::Templates->new($session{'CurrentUser'});
+$OtherTemplates->LimitToGlobal($DefaultQueue);
+
+</%INIT>
+<%ARGS>
+
+$Default => 'none'
+$DefaultQueue => undef
+$Name => 'Template'
+
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SelectUsers b/rt/webrt/Admin/Elements/SelectUsers
new file mode 100644 (file)
index 0000000..af51c60
--- /dev/null
@@ -0,0 +1,17 @@
+<SELECT MULTIPLE NAME="<%$Name%>"  SIZE=10>
+%while (my $user = $users->Next) {
+<OPTION VALUE="<%$user->id%>"><%$user->Name%>
+%}
+</SELECT>
+
+<%INIT>
+my $users = new RT::Users($session{'CurrentUser'});
+
+$users->Limit(FIELD => 'id', VALUE => $RT::SystemUser->id, OPERATOR => '!=' );
+$users->Limit(FIELD => 'id', VALUE => $RT::Nobody->id, OPERATOR => '!=' );
+$users->LimitToPrivileged();
+
+</%INIT>
+<%ARGS>
+$Name => 'Users'
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/SystemTabs b/rt/webrt/Admin/Elements/SystemTabs
new file mode 100755 (executable)
index 0000000..f8b2312
--- /dev/null
@@ -0,0 +1,31 @@
+<& /Admin/Elements/Tabs, subtabs => $subtabs, current_tab => 'Admin/Global/', current_subtab => $current_subtab &>
+<hr>
+<%INIT>
+  my $subtabs = {
+               
+              A => { title => 'Scrips',
+                          path => 'Admin/Global/Scrips.html',
+                        },
+              Ba => { title => 'Keyword Selections',           
+                               path => 'Admin/Global/Keywords.html',
+                     },
+
+              B => { title => 'Templates',
+                       path => 'Admin/Global/Templates.html',
+                     },
+               C => { title => 'Group Rights',
+                               path => 'Admin/Global/GroupRights.html',
+                     },
+               D => { title => 'User Rights',
+                               path => 'Admin/Global/UserRights.html',
+                     }
+
+
+};
+</%INIT>
+
+  
+<%ARGS>
+$id => undef
+$current_subtab => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/Tabs b/rt/webrt/Admin/Elements/Tabs
new file mode 100755 (executable)
index 0000000..ee6d82b
--- /dev/null
@@ -0,0 +1,31 @@
+<& /Elements/Tabs, tabs => $tabs, subtabs => $subtabs, current_toptab => 'Admin/', current_tab => $current_tab, current_subtab => $current_subtab&>
+
+<hr>
+  
+<%INIT>
+  my $tabs = { Users => { title => 'Users',
+                         path => 'Admin/Users/',
+                       },
+              Groups => { title => 'Groups',
+                          path => 'Admin/Groups/',
+                        },
+              Queues => { title => 'Queues',
+                          path => 'Admin/Queues/',
+                        },
+              System => { 'title' => 'Global',
+                          path => 'Admin/Global/',
+                        },
+               Keywords => { title => 'Keywords',
+                                   path => 'Admin/Keywords/',
+                                 },
+
+              
+            };
+</%INIT>
+
+
+<%ARGS>
+$subtabs => undef
+$current_tab => undef
+$current_subtab => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Elements/UserTabs b/rt/webrt/Admin/Elements/UserTabs
new file mode 100755 (executable)
index 0000000..bbf1731
--- /dev/null
@@ -0,0 +1,21 @@
+<& /Admin/Elements/Tabs, subtabs => $subtabs,
+                        current_tab => 'Admin/Users/', 
+                         current_subtab => $current_subtab &>
+<hr>
+<%INIT>
+my $subtabs = {
+              Queues => { title => 'Basics',
+                          path => "Admin/Users/Modify.html?id=".$id
+                        },
+#             Scrips => { title => 'Rights',
+#                         path => "Admin/Users/Rights.html?id=".$id
+#                       }
+              
+             };
+</%INIT>
+  
+  
+<%ARGS>
+$id => undef
+$current_subtab => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Global/GroupRights.html b/rt/webrt/Admin/Global/GroupRights.html
new file mode 100755 (executable)
index 0000000..26b7e1f
--- /dev/null
@@ -0,0 +1,78 @@
+<& /Admin/Elements/Header, Title => 'Modify System ACLS' &>
+<& /Admin/Elements/SystemTabs &>
+  
+<& /Elements/ListActions, actions => \@results &>
+  <FORM METHOD=POST action="GroupRights.html">
+      
+
+
+<h2>Modify global rights for groups</h2>
+
+<TABLE>
+<TR><TD>Pseudogroups</TD></TR>
+% while (my $GroupObj = $PseudoGroups->Next()) {
+             
+             <TR ALIGN=RIGHT> 
+               <TD VALIGN=TOP>
+                 <% $GroupObj->Name %>
+               </TD>
+               <TD>
+           <& /Admin/Elements/SelectRights, PrincipalObj => $GroupObj, 
+                                   PrincipalType => 'Group',
+                                   Scope => 'System' &>
+               </TD>
+             </TR>
+       
+% }
+
+<TR><TD>Groups</TD></TR>
+
+% while (my $GroupObj = $Groups->Next()) {
+             
+             <TR ALIGN=RIGHT> 
+               <TD VALIGN=TOP>
+                 <% $GroupObj->Name %>
+               </TD>
+               <TD>
+           <& /Admin/Elements/SelectRights, PrincipalObj => $GroupObj, 
+                                   PrincipalType => 'Group',
+                                   Scope => 'System' &>
+               </TD>
+             </TR>
+       
+% }
+       
+      </TABLE>
+    <& /Elements/Submit, Caption => "Be sure to save your changes", Reset => 1 &>
+  </FORM>
+  
+  <%INIT>
+   #Update the acls.
+ my @results =  ProcessACLChanges(\@CheckACL, \%ARGS);
+
+        
+    # {{{ do basic initialization.
+    
+  
+  
+  # Find out which groups we want to display ACL selects for.
+  my $Groups = new RT::Groups($session{'CurrentUser'});
+  #TODO: limit this to non-pseudogroups
+  $Groups->LimitToReal();
+
+
+  my $PseudoGroups = new RT::Groups($session{'CurrentUser'});
+  #TODO: limit this to non-pseudogroups
+  $PseudoGroups->LimitToPseudo;
+
+  # }}}
+    
+  
+
+  
+  </%INIT>
+
+<%ARGS>
+@CheckACL => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Global/Keywords.html b/rt/webrt/Admin/Global/Keywords.html
new file mode 100644 (file)
index 0000000..bf7bbd2
--- /dev/null
@@ -0,0 +1,97 @@
+<& /Admin/Elements/Header, Title => 'Edit keywords' &>
+<& /Admin/Elements/SystemTabs &>
+<& /Elements/ListActions, actions => \@actions &>
+
+<& /Elements/TitleBoxStart, title => $description &>
+  
+  <FORM METHOD=POST ACTION="Keywords.html">
+
+% if ($KeywordSelects->Count > 0 ) {
+<TABLE>
+<TR><TD>Delete</TD></TR>
+%  while (my $keywordselect = $KeywordSelects->Next ) {
+<TR>
+  <TD><INPUT TYPE="CHECKBOX" NAME="KeywordSelect-<%$keywordselect->Id%>-Delete"></TD>
+  <TD><& /Admin/Elements/SelectKeywordSelect, KeywordSelect => $keywordselect &></TD>
+</TR>
+%  }
+</TABLE>
+% }
+
+Add a global keyword selection:
+%my $ks = new RT::KeywordSelect($session{'CurrentUser'});
+<ul>
+<li><& /Admin/Elements/SelectKeywordSelect, KeywordSelect => $ks, NamePrefix => 'new' &></li>
+</ul>
+
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit &>
+</FORM>
+
+
+
+<%init>
+my (@actions);
+
+my $description = "Modify global Keyword selections";
+
+my $KeywordSelects = new RT::KeywordSelects ($session{'CurrentUser'});
+
+unless ($KeywordSelects->LimitToGlobals()) {
+    Abort("Couldn't load KeywordSelects.");
+}
+
+
+# {{{ if we're trying to create a new keyword select
+
+if ($ARGS{'KeywordSelect-new-Name'}) {
+    my $NewKeywordSelect = new RT::KeywordSelect($session{'CurrentUser'});
+    
+    my ($retval, $msg) = $NewKeywordSelect->Create ( Keyword => $ARGS{'KeywordSelect-new-Keyword'},
+                                            ObjectField => 'Queue',
+                                            ObjectType => 'Ticket',
+                                            ObjectValue => 0,
+                                            Name => $ARGS{'KeywordSelect-new-Name'},
+                                            Single => $ARGS{'KeywordSelect-new-Single'},
+                                            Depth => $ARGS{'KeywordSelect-new-Depth'}
+                                          );
+       push (@actions, $msg);
+}
+# }}}
+
+# {{{ if we're trying to delete the keywordselect
+foreach my $key (keys %ARGS) {
+    if ($key =~ /^KeywordSelect-(\d+)-Delete$/) {
+       my $id = $1;
+       my $keywordselect = new RT::KeywordSelect($session{'CurrentUser'});
+       $keywordselect->Load($id) || push @actions, "Couldn't load keywordSelect";
+       my ($val, $msg) = $keywordselect->SetDisabled(1);
+       if ($val) {
+           push @actions, 'KeywordSelect disabled.';
+       }       
+       else {
+           push @actions, $msg;
+       }       
+    }
+}
+# }}}
+# {{{ if we're modifying keyword selects
+my @fields = qw(Name Keyword Single Depth);
+
+while (my $ks = $KeywordSelects->Next) {
+    foreach my $field (@fields) {
+       if (defined ($ARGS{"KeywordSelect-".$ks->Id."-".$field}) &&
+           ($ARGS{"KeywordSelect-".$ks->Id."-".$field} ne $ks->$field())) {
+           
+           my $method = "Set$field";
+           my ($val, $msg) = $ks->$method($ARGS{"KeywordSelect-".$ks->Id."-".$field});
+           push @actions, "Keyword Select ". $ks->Name."/$field:".$msg;
+       }       
+    }
+}
+# }}}
+
+</%init>
+
+<%ARGS>
+</%ARGS>
diff --git a/rt/webrt/Admin/Global/Scrips.html b/rt/webrt/Admin/Global/Scrips.html
new file mode 100755 (executable)
index 0000000..e55f8b3
--- /dev/null
@@ -0,0 +1,95 @@
+<& /Admin/Elements/Header, Title => 'Edit scrips' &>
+<& /Admin/Elements/SystemTabs &>
+
+<& /Elements/ListActions, actions => \@actions &>
+
+<& /Elements/TitleBoxStart, title => "Modify global scrips" &>
+  
+  <FORM METHOD=POST ACTION="Scrips.html">
+
+% if ($Scrips->Count > 0 ) {
+<TABLE>
+<TR>
+<TD>Delete
+</TD>
+<TD>
+</TR>
+
+%   while (my $scrip = $Scrips->Next ) {
+<TR>
+<TD>
+<INPUT TYPE="CHECKBOX" NAME="DeleteScrip-<%$scrip->Id%>">
+</TD>
+<TD>
+<% $scrip->ConditionObj->Name %> 
+<% $scrip->ActionObj->Name %> 
+with template <% $scrip->TemplateObj->Name %>
+</TD>
+</TR>
+%   }
+
+</TABLE>
+
+% }
+Add a scrip which will apply to all queues:
+<ul>
+<li>Condition: <& /Admin/Elements/SelectScripCondition, Name => 'NewScripCondition' &>
+         Action: <& /Admin/Elements/SelectScripAction, Name => 'NewScripAction' &>
+         Template: <& /Admin/Elements/SelectTemplate, Name => 'NewScripTemplate' &>
+
+</ul>
+
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit &>
+</FORM>
+<%init>
+my (@actions, $description);
+
+my $Scrips = new RT::Scrips ($session{'CurrentUser'});
+$Scrips->LimitToGlobal();
+
+
+
+
+if ($NewScripAction and $NewScripCondition) {
+    my $NewScrip = new RT::Scrip($session{'CurrentUser'});
+    
+    my ($retval, $msg) = $NewScrip->Create ( ScripAction => $NewScripAction,
+                                    ScripCondition => $NewScripCondition,
+                                    Stage => 'TransactionCreate',
+                                    Queue => 0,
+                                    Template => $NewScripTemplate);
+    if (defined $retval) {
+        push @actions, $msg;
+    }
+    else {
+        push @actions, $msg;
+    }
+}
+
+# {{{ deal with modifying and deleting existing scrips
+my ($key );
+foreach $key (keys %ARGS) {
+  # {{{ if we're trying to delete the scrip
+  if ($key =~ /^DeleteScrip-(\d+)/) {
+    my $id = $1;
+    my $scrip = new RT::Scrip($session{'CurrentUser'});
+    $scrip->Load($id);
+    my ($retval, $msg) = $scrip->Delete;
+    if ($retval) {
+      push @actions, "Scrip deleted";
+    }
+    else {
+      push @actions, $msg;
+    }
+  }
+  # }}}
+}
+# }}}
+</%init>
+
+<%ARGS>
+$NewScripCondition => undef
+$NewScripAction => undef
+$NewScripTemplate => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Global/Template.html b/rt/webrt/Admin/Global/Template.html
new file mode 100755 (executable)
index 0000000..856d2ee
--- /dev/null
@@ -0,0 +1,66 @@
+<& /Admin/Elements/Header, title => "Modify template ".$TemplateObj->id&>
+<& /Admin/Elements/SystemTabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<& /Elements/TitleBoxStart, title => $title &>
+
+<FORM METHOD=POST ACTION="Template.html">
+%if ($create ) {
+<INPUT TYPE=HIDDEN NAME=template VALUE="new">
+% } else {
+<INPUT TYPE=HIDDEN NAME=template VALUE="<%$TemplateObj->Id%>">
+% }
+
+%# hang onto the queue id
+<INPUT TYPE=HIDDEN name="Queue" value="<%$Queue%>">
+
+
+Name: <input name="Name" VALUE="<%$TemplateObj->Name%>" SIZE=20><BR>
+Description: <input name="Description" VALUE="<%$TemplateObj->Description%>" SIZE=80><BR>
+
+<TEXTAREA NAME=Content ROWS=25 COLS=80 WRAP=SOFT>
+<%$TemplateObj->Content%></TEXTAREA>
+
+<& /Elements/TitleBoxEnd&>
+<&/Elements/Submit&>
+</FORM>
+
+
+
+<%INIT>
+
+my $TemplateObj = new RT::Template($session{'CurrentUser'});
+my  ($title, @results);
+
+if ($create) {
+  $title = "Create a template";
+}
+
+else {
+  if ($template eq 'new') {
+      my ($val, $msg) =  $TemplateObj->Create(Queue => $Queue, Name => $Name);
+      Abort("Could not create template: $msg") unless ($val);
+     push @results, $msg;
+     $title = 'Created template ' . $TemplateObj->Name(); 
+    }
+    else {
+       $TemplateObj->Load($template) || Abort('No Template');
+      $title = 'Editing template ' . $TemplateObj->Name(); 
+    }
+  
+    
+}
+if ($TemplateObj->Id()) {
+  my @attribs = qw( Description Content Queue Name);
+  my @aresults = UpdateRecordObject( AttributesRef => \@attribs, 
+                                    Object => $TemplateObj, 
+                                    ARGSRef => \%ARGS);
+  push @results, @aresults;
+}
+</%INIT>
+<%ARGS>
+$Queue => undef
+$template => undef
+$create => undef
+$Name => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Global/Templates.html b/rt/webrt/Admin/Global/Templates.html
new file mode 100755 (executable)
index 0000000..cf388e5
--- /dev/null
@@ -0,0 +1,24 @@
+<& /Admin/Elements/Header, Title => 'Edit system templates' &>
+<& /Admin/Elements/SystemTabs &>
+
+<& /Elements/TitleBoxStart, title => 'Edit system templates' &>
+<UL>
+<LI><A href="Template.html?create=1&Queue=0">Create a new template</A><BR><BR>
+
+
+%while  (my $TemplateObj = $Templates->Next) { 
+
+<LI><A HREF="Template.html?template=<%$TemplateObj->id()%>"><%$TemplateObj->id()%>/<%$TemplateObj->Name%>: <%$TemplateObj->Description%></a><BR>
+
+%}
+
+<& /Elements/TitleBoxEnd &>
+<%INIT>
+
+my $Templates = RT::Templates->new($session{'CurrentUser'});
+$Templates->LimitToGlobal();
+
+</%INIT>
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Global/UserRights.html b/rt/webrt/Admin/Global/UserRights.html
new file mode 100755 (executable)
index 0000000..351f4b8
--- /dev/null
@@ -0,0 +1,42 @@
+<& /Admin/Elements/Header, Title => 'Modify System ACLS' &>
+<& /Admin/Elements/SystemTabs &>
+  
+<& /Elements/ListActions, actions => \@results &>
+  <FORM METHOD=POST action="UserRights.html">
+      
+
+<h2>Modify global rights for users</h2>
+<TABLE>
+%      while (my $UserObj = $Users->Next()) {
+  <TR ALIGN=RIGHT> 
+       <TD VALIGN=TOP>
+         <A HREF="<%$RT::WebPath%>/Admin/Users/Modify.html?id=<%$UserObj->id%>"><% $UserObj->Name %></A>
+       </TD>
+       <TD>
+         <& /Admin/Elements/SelectRights, PrincipalObj => $UserObj, 
+             PrincipalType => 'User',
+             Scope => 'System' &>
+         
+       </TD>
+      </TR>
+       
+% }
+    </TABLE>
+    
+    <& /Elements/Submit, Caption => "Be sure to save your changes", Reset => 1 &>
+  </FORM>
+  
+<%INIT>
+ my @results =  ProcessACLChanges(\@CheckACL, \%ARGS);
+
+ # Find out which users we want to display ACL selects for
+ my $Users = new RT::Users($session{'CurrentUser'});
+
+ $Users->LimitToPrivileged();
+  
+</%INIT>
+
+<%ARGS>
+@CheckACL => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Global/index.html b/rt/webrt/Admin/Global/index.html
new file mode 100755 (executable)
index 0000000..5907ed1
--- /dev/null
@@ -0,0 +1,2 @@
+<& /Admin/Elements/Header, Title => 'Admin/Global configuration' &>
+<& /Admin/Elements/SystemTabs &>
diff --git a/rt/webrt/Admin/Groups/Members.html b/rt/webrt/Admin/Groups/Members.html
new file mode 100644 (file)
index 0000000..4b0e0d0
--- /dev/null
@@ -0,0 +1,76 @@
+<& /Admin/Elements/Header, Title => "RT/Admin/Edit the group ". $Group->Name &>
+<& /Admin/Elements/GroupTabs, GroupObj => $Group &>
+<& /Elements/ListActions, actions => \@results &>
+
+
+<& /Elements/TitleBoxStart, title => 'Editing membership for group '.$Group->Name &>
+
+<FORM ACTION="<%$RT::WebPath%>/Admin/Groups/Members.html" METHOD=POST>
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$Group->Id%>">
+<TABLE WIDTH="100%">
+<TR>
+<TD>
+Add members
+</TD>
+<TD>
+Current members
+</TD>
+</TR>
+
+<TR>
+<TD VALIGN=TOP>
+<& /Admin/Elements/SelectUsers, Name => "AddMembers" &>
+</TD>
+<TD VALIGN=TOP>
+% if ($Group->MembersObj->Count == 0 ) {
+<i>(No members)</i>
+% } else {
+(Check box to delete group member)
+<UL>
+% while (my $member = $Group->MembersObj->Next()) {
+<LI><INPUT TYPE=CHECKBOX Name="DeleteMember-<%$member->UserObj->id%>">
+<%$member->UserObj->Name%> (<%$member->UserObj->RealName%>)
+% }
+% }
+</UL>
+</TD>
+</TR>
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit &>
+</form>
+
+
+<%INIT>
+
+my $Group = new RT::Group($session{'CurrentUser'});
+$Group->Load($id) || Abort('Could not load group');
+
+my (@results);
+
+my $key;
+foreach $key (keys %ARGS) {
+
+if ($key =~ /^DeleteMember-(\d+)$/) {
+    my $id = $1; 
+    my ($val,$msg) = $Group->DeleteMember($id);
+    push (@results, $msg);
+}
+}
+
+# Make sure AddMembers is always an array
+my @AddMembers = (ref $AddMembers eq 'ARRAY') ? @{$AddMembers} : ($AddMembers);
+
+foreach my $member (@AddMembers) {
+    next unless ($member);
+    my ($val, $msg) = $Group->AddMember($member);
+    push (@results, $msg);
+}
+
+
+</%INIT>
+
+<%ARGS>
+$AddMembers => undef
+$id => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Groups/Modify.html b/rt/webrt/Admin/Groups/Modify.html
new file mode 100644 (file)
index 0000000..7104a69
--- /dev/null
@@ -0,0 +1,83 @@
+<& /Admin/Elements/Header, Title => $title &>
+
+<& /Admin/Elements/GroupTabs, GroupObj => $Group &>
+<& /Elements/ListActions, actions => \@results &>
+
+
+<& /Elements/TitleBoxStart, title => $title &>
+
+<FORM ACTION="<%$RT::WebPath%>/Admin/Groups/Modify.html" METHOD=POST>
+
+%unless ($Group->Id) {
+<INPUT TYPE=HIDDEN NAME=id VALUE="new">
+% } else {
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$Group->Id%>">
+% }
+<TABLE>
+<TR><TD ALIGN=RIGHT>
+Name: 
+</TD>
+<TD><INPUT name="Name" value="<%$Group->Name%>"></TD>
+</TR><TR>
+<TD ALIGN=RIGHT>
+Description:</TD><TD COLSPAN=3><INPUT name="Description" value="<%$Group->Description%>" size=60></TD></TR>
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+
+<& /Elements/Submit &>
+</form>
+<%INIT>
+
+my ($title);
+my (@results);
+
+my $Group = new RT::Group($session{'CurrentUser'});
+
+if ($Create) {
+    $title = "Create a new group";
+} 
+
+else {
+    
+    if ($id eq 'new' ) {
+       
+       $Group->Create(Name => "$Name") || Abort ("Group could not be created.");
+       $id = $Group->Id;
+    }
+    else {
+       $Group->Load($id) || Abort('Could not load group');
+    }
+
+
+    if ($id) {
+       $title = "Modify the group ". $Group->Name;
+
+    }  
+
+    # If the create failed
+    else {
+       $title = "Create a new group";
+       $Create = 1;
+    }    
+    
+}
+
+if ($id) {
+    
+    my @fields = qw(Description Name );
+    my @fieldresults = UpdateRecordObject ( AttributesRef => \@fields,
+                                           Object => $Group,
+                                           ARGSRef => \%ARGS );
+    push (@results,@fieldresults);
+}
+
+
+</%INIT>
+
+
+<%ARGS>
+$Create => undef
+$Name => undef
+$Description => undef
+$id => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Groups/Rights.html b/rt/webrt/Admin/Groups/Rights.html
new file mode 100644 (file)
index 0000000..5c842a3
--- /dev/null
@@ -0,0 +1 @@
+Not yet implemented....
diff --git a/rt/webrt/Admin/Groups/index.html b/rt/webrt/Admin/Groups/index.html
new file mode 100644 (file)
index 0000000..d419e7f
--- /dev/null
@@ -0,0 +1,33 @@
+
+<& /Admin/Elements/Header, Title => 'Admin/Groups' &>
+<& /Admin/Elements/Tabs, current_tab => 'Admin/Groups/' &>
+
+<& /Elements/TitleBoxStart, title => 'Select a group' &>
+
+Pseudogroups:<BR>
+<UL>
+%while ( $Group = $PseudoGroups->Next) {
+<LI><A HREF="Modify.html?id=<%$Group->id%>"><%$Group->Name%></a><BR>
+%}
+
+</UL>
+
+Groups:<BR>
+<UL>
+<LI><A HREF="Modify.html?Create=1">Create a new group</A><BR><BR></LI>
+%while ( $Group = $Groups->Next) {
+<LI><A HREF="Modify.html?id=<%$Group->id%>"><%$Group->Name%></a><BR>
+%}
+</UL>
+
+<& /Elements/TitleBoxEnd &>
+<%INIT>
+my ($Group);
+my $PseudoGroups = new RT::Groups($session{'CurrentUser'});
+$PseudoGroups->LimitToPseudo;
+my $Groups = new RT::Groups($session{'CurrentUser'});
+$Groups->LimitToReal;
+
+</%INIT>
+<%ARGS>
+</%ARGS>
diff --git a/rt/webrt/Admin/KeywordSelects/Modify.html b/rt/webrt/Admin/KeywordSelects/Modify.html
new file mode 100644 (file)
index 0000000..e753c66
--- /dev/null
@@ -0,0 +1,17 @@
+<& /Admin/Elements/Header, Title => 'Admin KeywordSelects' &>
+<& /Admin/Elements/Tabs &>
+
+<& /Admin/Elements/ModifyKeywordSelect, Create=>$Create, id=>$id, Parent=>$Parent, ObjectType=>$ObjectType, ObjectField=>$ObjectField, ObjectValue=>$ObjectValue, Single=>$Single, SingleMagic=>$SingleMagic, Generations=>$Generations &>
+
+<%ARGS>
+$Create => undef
+$id => undef
+$Parent => undef
+$ObjectType => undef
+$ObjectField => undef
+$ObjectValue => undef
+$Single => undef
+$SingleMagic => undef
+$Generations => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/KeywordSelects/index.html b/rt/webrt/Admin/KeywordSelects/index.html
new file mode 100644 (file)
index 0000000..ba3da9f
--- /dev/null
@@ -0,0 +1,137 @@
+<& /Admin/Elements/Header, Title => 'Admin KeywordSelects' &>
+<& /Admin/Elements/Tabs, current_tab => 'Admin/KeywordSelects/' &>
+
+A <B>KeywordSelect</B> is a link between a <B>Keyword</B> and a object
+(currently just <B>Tickets</B>), titled by the <I>Name</I> field of the Keyword such that:
+<ul>
+<li>Object display will contain a field, titled with the <I>Name</I> field of
+the <B>Keyword</B> and showing any descendent keywords.
+<li>Object creation for this field will contain a field, titled with the
+<I>Name</I> field of the <B>Keyword</B> and containing the descendents of
+the <B>Keyword</B> as choices.
+<li>Searches for this object type will contain a selection field titled with
+the <I>Name</I> field of the <B>Keyword</B> and containing the descendents
+of the <B>Keyword</B> as choices.
+<TABLE WIDTH=100%>
+
+
+
+  <TD VALIGN=TOP>
+    <h2>Create KeywordSelect</h2>
+  <FORM NAME="ModifyKeywordSelect" METHOD="POST" ACTION="<%$RT::WebPath%>/Admin/KeywordSelects/Modify.html">
+    [<%$title |n %>]
+    <BR>
+      
+      <INPUT TYPE="hidden" NAME="id" VALUE="<% $id %>">
+       Keyword 
+       <SELECT NAME="Parent">
+         
+%while ( $parent = $parents->Next ) {
+         
+         <OPTION VALUE="<% $parent->id %>" <% defined($KeywordSelect->Parent) && $parent->id == $KeywordSelect->Parent ? ' SELECTED' : '' %>><% $parent->Name %></OPTION>
+         
+% }
+         
+       </SELECT>
+       <BR>
+         Object 
+         <SELECT NAME="ObjectType">
+           <OPTION SELECTED>Ticket</OPTION>
+         </SELECT>
+         <BR>
+           
+%foreach $queue ( keys %queues ) {
+    addOption("<% $queues{$queue} %>", "<% $queue %>", false, <% $queue == $KeywordSelect->ObjectValue ? 'true' : 'false' %> )
+%}
+  }
+}
+</SCRIPT>
+           
+           Limit to <SELECT NAME="ObjectField" onChange="ChangeObjectValue(this)">
+             <OPTION VALUE="" <% $KeywordSelect->ObjectField ? '' : ' SELECTED' %>>(none)</OPTION>
+             <OPTION VALUE="Queue" <% $KeywordSelect->ObjectField eq 'Queue' ? ' SELECTED' : '' %>>Queue</OPTION>
+           </SELECT> 
+           <SELECT NAME="ObjectValue">
+             <OPTION VALUE="<% $KeywordSelect->ObjectValue %>">
+               <% $KeywordSelect->ObjectField ? $queues{$KeywordSelect->ObjectValue} : "(n/a)" %></OPTION>
+           </SELECT><BR>
+             <INPUT TYPE="hidden" NAME="SingleMagic" VALUE="1">
+               <INPUT TYPE="checkbox" NAME="Single" VALUE="1" <% $KeywordSelect->Single ? ' CHECKED' : '' %>>Allow single selection only<BR>
+                   Limit to <INPUT TYPE="text" NAME="Generations" SIZE="2" VALUE="<% $KeywordSelect->Generations %>"> generations (0 = no limit)<BR>
+                       <& /Elements/Submit, Label => $submit &>
+
+</FORM>
+
+<%INIT>
+
+
+my $KeywordSelect = new RT::KeywordSelect($session{CurrentUser});
+  
+my($title, $submit);
+  
+if ( $Create ) {
+      $title = "Create a new KeywordSelect";
+      $submit = "Create";
+      $id = "new";
+} else {
+    if  ( $id eq 'new' ) {
+       $id = $KeywordSelect->Create (
+                                     Parent      => $Parent,
+                                     ObjectType  => $ObjectType,
+                                     ObjectField => $ObjectField,
+                                     ObjectValue => $ObjectValue,
+                                     Single      => $Single,
+                                     Generations => $Generations,
+                                    ) or Abort "can't create KeywordSelect";
+    } else {
+       $KeywordSelect->Load($id) || Abort("Can't load keyword id $id");
+       #false laziness
+       $KeywordSelect->SetParent($Parent) if defined($Parent);
+       $KeywordSelect->SetObjectType($ObjectType) if defined($ObjectType);
+       $KeywordSelect->SetObjectField($ObjectField) if defined($ObjectField);
+       $KeywordSelect->SetObjectValue($ObjectValue) if defined($ObjectValue);
+       $KeywordSelect->SetSingle($Single) if defined($SingleMagic);
+       $KeywordSelect->SetGenerations($Generations) if defined($Generations);
+    }
+    $title = "Modify the KeywordSelect <B>". $KeywordSelect->KeywordObj->Name. "</B>";
+    $submit = "Modify";
+    
+}
+  
+  my $parents = new RT::Keywords($session{CurrentUser});
+  $parents->UnLimit;
+  my $parent;
+
+my $queues = new RT::Queues($session{CurrentUser});
+$queues->UnLimit;
+
+my %queues;
+my $queue;
+$queues{$queue->id} = $queue->Name while $queue = $queues->Next;
+
+</%INIT>
+
+<%ARGS>
+$id => undef
+$Create => undef
+$Parent => undef
+$ObjectType => undef
+$ObjectField => undef
+$ObjectValue => undef
+$Single => undef
+$SingleMagic => undef
+$Generations => undef
+</%ARGS>
+
+
+    <& /Admin/Elements/ModifyKeywordSelect, 'Create'=>'1' &>
+  </TD>
+
+  <TD VALIGN=TOP>
+    <H2>Modify KeywordSelect</H2>
+
+    <& /Admin/Elements/SelectModifyKeywordSelect &>
+  </TD>
+</TR>
+
+</TABLE>
diff --git a/rt/webrt/Admin/Keywords/Modify.html b/rt/webrt/Admin/Keywords/Modify.html
new file mode 100644 (file)
index 0000000..bb7e2db
--- /dev/null
@@ -0,0 +1,96 @@
+<& /Admin/Elements/Header, Title => $title &>
+<& /Admin/Elements/Tabs &>
+
+
+<& /Elements/TitleBoxStart, title => %$title  &>
+<FORM METHOD="POST" ACTION="<%$RT::WebPath%>/Admin/Keywords/Modify.html">
+<INPUT TYPE="hidden" NAME="id" VALUE="<% $id %>">
+Keyword <INPUT NAME="Name" VALUE="<% $Keyword->Name %>"><BR>
+
+Parent <SELECT NAME="Parent">
+             <OPTION VALUE=""<% defined($Keyword->Parent) ? '' : ' SELECTED' %>>-</OPTION>
+%while ( $parent = $parents->Next ) {
+             <OPTION VALUE="<% $parent->id %>"<% defined($Keyword->Parent) && $parent->id == $Keyword->Parent ? ' SELECTED' : '' %>><% $parent->Name %></OPTION>
+%}
+</SELECT>
+
+
+New children of this keyword. one per line.
+<TEXTAREA NAME="Kids" ROWS=4><% $kidstring %></TEXTAREA>
+<BR>
+
+<& /Elements/Submit, Label => $submit &>
+</FORM>
+<& /Elements/TitleBoxEnd &>
+
+<%INIT>
+
+my $Keyword = new RT::Keyword($session{CurrentUser});
+my ($title, $submit, %kids, $kid);
+
+if ( $Create ) {
+    $title = "Create a new Keyword";
+    $submit = "Create";
+    $id = "new";
+    %kids = ();
+    $Parent = ''; #silence 
+} 
+else {
+    if ( $id eq 'new' ) {
+       $id = $Keyword->Create( Name => $Name, Parent => $Parent )
+         or Abort("can't create keyword Name=>$Name, Parent=>$Parent");
+    } else {
+       $Keyword->Load($id) || Abort("Can't load keyword id $id");
+       
+       #foreach my $field ( grep eval "defined(\$$_)", qw( Name Parent )) {
+       #  eval "\$Keyword->Set(\$field=>\$$field); #sigh
+       #}
+       
+       $Keyword->SetName($Name) if defined($Name);
+       $Keyword->SetParent($Parent) if defined($Parent);
+    }
+
+    $title = "Modify the Keyword <B>". $Keyword->Name. "</B>";
+    $submit = "Modify";
+}
+
+
+my $kids = $Keyword->Children(new RT::Keywords($session{CurrentUser}));
+
+$kids{$kid->Name} = $kid while $kid = $kids->Next;
+
+if ( defined($Kids) ) {
+    my %newkids;
+
+       %newkids = map { $_=>1 } split(/\r/, $Kids);
+
+    }
+    foreach ( grep { ! defined($newkids{$_}) } keys %kids ) {
+       $kids{$_}->Delete;
+       delete $kids{$_};
+    }
+    foreach ( grep { ! defined($kids{$_}) } keys %newkids ) {
+       $kids{$_} = new RT::Keyword($session{CurrentUser});
+       $kids{$_}->Create( Name => $_, Parent => $id )
+         or Abort("can't create keyword Name=>$_, Parent=>$id");
+    }
+
+}
+
+
+my $parent;
+my $parents = new RT::Keywords($session{CurrentUser});
+$parents->UnLimit;
+
+my $kidstring = join("\r", keys %kids);
+
+</%INIT>
+
+<%ARGS>
+$id => undef
+$Create => undef
+$Name => undef
+$Parent => undef
+$Kids => undef
+
+</%ARGS>
diff --git a/rt/webrt/Admin/Keywords/index.html b/rt/webrt/Admin/Keywords/index.html
new file mode 100644 (file)
index 0000000..12814ec
--- /dev/null
@@ -0,0 +1,110 @@
+<& /Elements/Header, Title => 'Admin/Keywords' &>
+<& /Admin/Elements/Tabs, current_tab => 'Admin/Keywords/' &>
+
+<& /Elements/ListActions, actions => \@Actions &>
+
+<& /Elements/TitleBoxStart, title => 'Keywords' &>
+<a href="<%$RT::WebPath%>/Admin/Keywords/?RootId=<%$Root->Parent%>"><%$Root->Path%></a>
+<UL>
+<FORM METHOD=POST ACTION="index.html">
+<input type=hidden name=RootId value="<%$RootId%>">
+
+
+% while (my $key = $Keywords->Next) {
+    <LI>
+% if ($Edit == $key->id) {
+      <input name="KeyName-<%$key->id%>" value="<%$key->Name%>">
+         <input type=submit value="Update">
+           <input type=submit name="Disable-<%$key->id%>" value="Disable">
+% } else {
+             <A HREF="?RootId=<%$key->id%>"><%$key->Name%></A>
+% if ($key->Disabled) {
+       <input type=submit name="Enable-<%$key->id%>" value="Enable">
+% } else {
+      [<a href="?Edit=<%$key->id%>&RootId=<%$Root->Id%>">edit</a>]
+% }
+% }
+
+
+         </LI>
+% }
+         <LI>
+           <input name="KeyName-New"> <input type=submit value="Add">
+</UL>
+<BR>
+         <input type="checkbox" name="ShowDisabled"> Include disabled items in listing.
+       <input type=submit value="Go!">
+
+</FORM>
+
+<& /Elements/TitleBoxEnd &>
+<%INIT>
+my (@Actions);
+  
+if ($ARGS{'KeyName-New'}) {
+    my $NewKey = new RT::Keyword($session{'CurrentUser'});
+    my ($val, $msg) = $NewKey->Create( Parent => $RootId, Name => $ARGS{'KeyName-New'});
+    push (@Actions, $msg);
+}
+
+my $arg;
+foreach $arg (keys %ARGS) {
+    if ($arg =~ /^Disable-(\d*)$/) {
+       my $id = $1;
+       my $keyword = new RT::Keyword($session{'CurrentUser'});
+       $keyword->Load($id);
+       my ($val, $msg) = $keyword->SetDisabled(1);
+       push (@Actions, $msg);
+
+
+    }  
+    elsif ($arg =~ /^Enable-(\d*)$/) {
+       my $id = $1;
+       my $keyword = new RT::Keyword($session{'CurrentUser'});
+       $keyword->Load($id);
+       my ($val, $msg) = $keyword->SetDisabled(0);
+       push (@Actions, $msg);
+    }
+    elsif ($arg =~ /^KeyName-(\d*)$/) {
+       my $id = $1;
+       my $keyword = new RT::Keyword ($session{'CurrentUser'});
+       $keyword->Load($id);
+       if ($keyword->Name() ne $ARGS{"$arg"}) {
+           my ($val, $msg) = $keyword->SetName($ARGS{"$arg"});
+           push (@Actions, $msg);
+       }       
+       if (($ARGS{"KeyParent-$id"}) && 
+           ($keyword->Parent ne $ARGS{"KeyParent-$id"})) {
+           my ($val, $msg) = $keyword->SetParent($ARGS{"KeyParent-$id"});
+           push (@Actions, $msg);
+       }       
+    }  
+}
+
+
+my $Root = new RT::Keyword($session{'CurrentUser'});
+my $Keywords;
+#If we have a root load it.
+if ($RootId != 0) {
+    $Root->Load($RootId);
+    $Keywords = $Root->Children();
+    
+}
+else {
+    $Keywords = new RT::Keywords($session{'CurrentUser'});
+    $Keywords->LimitToParent(0);
+}
+
+if ($ShowDisabled) {
+    $Keywords->{'find_disabled_rows'} = 1;
+}
+
+
+
+
+</%INIT>
+<%ARGS>
+$RootId => 0
+$Edit => undef
+$ShowDisabled => 0
+</%ARGS>
diff --git a/rt/webrt/Admin/Queues/Create.html b/rt/webrt/Admin/Queues/Create.html
new file mode 100755 (executable)
index 0000000..b39d659
--- /dev/null
@@ -0,0 +1,13 @@
+<& /Admin/Elements/Header, Title => 'Create a queue' &>
+    <h1>Create a queue</h1>
+
+<& /Admin/Elements/ModifyQueue, QueueObj => $QueueObj &>
+
+<%INIT>
+my $QueueObj = new RT::Queue($session{'CurrentUser'});
+$QueueObj->Create(Name => "$Name");
+</%INIT>
+
+<%ARGS>
+$Name => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Queues/GroupRights.html b/rt/webrt/Admin/Queues/GroupRights.html
new file mode 100755 (executable)
index 0000000..a2c6690
--- /dev/null
@@ -0,0 +1,103 @@
+<& /Admin/Elements/Header, Title => 'Modify group rights for queue '. $QueueObj->Name &>
+<& /Admin/Elements/QueueTabs, id => $id &>  
+<& /Elements/ListActions, actions => \@results &>
+
+  <FORM METHOD=POST ACTION="GroupRights.html">
+    <INPUT TYPE=HIDDEN NAME=id VALUE="<% $QueueObj->id %>">
+      
+
+
+<& /Elements/TitleBoxStart, title => 'Modify group rights for queue '.$QueueObj->Name &>
+
+<TABLE>
+<TR><TD>Pseudogroups</TD></TR>
+% while (my $GroupObj = $PseudoGroups->Next()) {
+             
+             <TR ALIGN=RIGHT> 
+               <TD VALIGN=TOP>
+                 <% $GroupObj->Name %>
+               </TD>
+               <TD>
+           <& /Admin/Elements/SelectRights, PrincipalObj => $GroupObj, 
+                                   PrincipalType => 'Group',
+                                  QueueObj => $QueueObj,
+                                   Scope => 'Queue' &>
+
+               </TD>
+             </TR>
+       
+% }
+
+<TR><TD>Groups</TD></TR>
+
+% while (my $GroupObj = $Groups->Next()) {
+             
+             <TR ALIGN=RIGHT> 
+               <TD VALIGN=TOP>
+                 <% $GroupObj->Name %>
+               </TD>
+               <TD>
+           <& /Admin/Elements/SelectRights, PrincipalObj => $GroupObj, 
+                                   PrincipalType => 'Group',
+                                  QueueObj => $QueueObj,
+                                   Scope => 'Queue' &>
+
+               </TD>
+             </TR>
+       
+% }
+       
+      </TABLE>
+
+      <& /Elements/TitleBoxEnd &>
+      <& /Elements/Submit, Caption => "Be sure to save your changes", Reset => 1 &>
+  </FORM>
+  
+<%INIT>
+#Update the acls.
+my @results =  ProcessACLChanges(\@CheckACL, \%ARGS);
+
+# {{{ Deal with setting up the display of current rights.
+
+# {{{ do basic initialization.
+
+#Define vars used in html above
+my ($GroupObj);
+
+my ($right);
+
+
+if (!defined $id) {
+    Abort("No Queue defined");
+}
+
+my $QueueObj = new RT::Queue($session{'CurrentUser'});
+$QueueObj->Load($id) ||
+  Abort("Couldn't load queue $id");
+  
+  # Find out which groups we want to display ACL selects for.
+  my $Groups = new RT::Groups($session{'CurrentUser'});
+  #TODO: limit this to non-pseudogroups
+  $Groups->LimitToReal();
+
+
+  my $PseudoGroups = new RT::Groups($session{'CurrentUser'});
+  #TODO: limit this to non-pseudogroups
+  $PseudoGroups->LimitToPseudo;
+
+
+# }}}
+    
+  
+  # }}}
+
+</%INIT>
+
+<%ARGS>
+$id => undef
+$UserString => undef
+$UserOp => undef
+$UserField => undef
+@CheckACL => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Queues/Keywords.html b/rt/webrt/Admin/Queues/Keywords.html
new file mode 100644 (file)
index 0000000..7809805
--- /dev/null
@@ -0,0 +1,114 @@
+<& /Admin/Elements/Header, Title => 'Edit keywords' &>
+<& /Admin/Elements/QueueTabs, id => $QueueObj->Id &>
+
+<& /Elements/ListActions, actions => \@actions &>
+
+<& /Elements/TitleBoxStart, title => $description &>
+
+<h2>Global Keyword Selections</h2>
+<& /Admin/Elements/ListGlobalKeywordSelects &>
+<BR>
+  
+  <FORM METHOD=POST ACTION="Keywords.html">
+    <INPUT TYPE=HIDDEN NAME=id VALUE="<%$id%>">
+
+% if ($KeywordSelects->Count > 0 ) {
+
+
+<h2>Queue Keyword Selections</h2>
+<TABLE>
+<TR><TD>Delete</TD></TR>
+%  while (my $keywordselect = $KeywordSelects->Next ) {
+<TR>
+  <TD><INPUT TYPE="CHECKBOX" NAME="KeywordSelect-<%$keywordselect->Id%>-Delete"></TD>
+  <TD><& /Admin/Elements/SelectKeywordSelect, KeywordSelect => $keywordselect &></TD>
+</TR>
+%  }
+</TABLE>
+% }
+
+Add a keyword selection to this queue:
+%my $ks = new RT::KeywordSelect($session{'CurrentUser'});
+<ul>
+<li><& /Admin/Elements/SelectKeywordSelect, KeywordSelect => $ks, NamePrefix => 'new' &></li>
+</ul>
+
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit &>
+
+</FORM>
+<%init>
+my (@actions);
+
+
+
+my $KeywordSelects = new RT::KeywordSelects ($session{'CurrentUser'});
+unless ($id =~ /^\d+$/) {
+    Abort("$id isn't a valid Queue id.");
+}
+
+unless ($KeywordSelects->LimitToQueue($id)) {
+    Abort("Couldn't load KeywordSelects.");
+}
+
+my $QueueObj = new RT::Queue($session{'CurrentUser'});
+$QueueObj->Load($id);
+
+my $description = "Modify Keyword selections for queue '". $QueueObj->Name ."'";
+
+
+
+# {{{ if we're trying to create a new keyword select
+
+if ($ARGS{'KeywordSelect-new-Name'}) {
+    my $NewKeywordSelect = new RT::KeywordSelect($session{'CurrentUser'});
+    
+    my ($retval, $msg) = $NewKeywordSelect->Create ( Keyword => $ARGS{'KeywordSelect-new-Keyword'},
+                                            ObjectField => 'Queue',
+                                            ObjectType => 'Ticket',
+                                            ObjectValue => $QueueObj->Id,
+                                            Name => $ARGS{'KeywordSelect-new-Name'},
+                                            Single => $ARGS{'KeywordSelect-new-Single'},
+                                            Depth => $ARGS{'KeywordSelect-new-Depth'}
+                                          );
+       push (@actions, $msg);
+}
+# }}}
+# {{{ if we're trying to delete the keywordselect
+foreach my $key (keys %ARGS) {
+    if ($key =~ /^KeywordSelect-(\d+)-Delete$/) {
+       my $id = $1;
+       my $keywordselect = new RT::KeywordSelect($session{'CurrentUser'});
+       $keywordselect->Load($id) || push @actions, "Couldn't load keywordSelect";
+       my ($val, $msg) = $keywordselect->SetDisabled(1);
+       if ($val) {
+           push @actions, 'KeywordSelect disabled.';
+       }       
+       else {
+           push @actions, $msg;
+       }       
+    }
+}
+# }}}
+# {{{ if we're modifying keyword selects
+my @fields = qw(Name Keyword Single Depth);
+
+while (my $ks = $KeywordSelects->Next) {
+    foreach my $field (@fields) {
+       if (defined ($ARGS{"KeywordSelect-".$ks->Id."-".$field}) &&
+           ($ARGS{"KeywordSelect-".$ks->Id."-".$field} ne $ks->$field())) {
+           
+           my $method = "Set$field";
+           my ($val, $msg) = $ks->$method($ARGS{"KeywordSelect-".$ks->Id."-".$field});
+           push @actions, "Keyword Select ". $ks->Name."/$field:".$msg;
+       }       
+    }
+}
+# }}}
+
+</%init>
+
+<%ARGS>
+$id => undef         #some identifier that a Queue could 
+
+</%ARGS>
diff --git a/rt/webrt/Admin/Queues/Modify.html b/rt/webrt/Admin/Queues/Modify.html
new file mode 100755 (executable)
index 0000000..7a200df
--- /dev/null
@@ -0,0 +1,137 @@
+<& /Admin/Elements/Header, Title => 'Admin/Queue/Basics' &>
+<& /Admin/Elements/QueueTabs, id => $QueueObj->id &>
+<& /Elements/ListActions, actions => \@results &>
+
+
+
+<& /Elements/TitleBoxStart, title => $title &>
+
+
+<FORM ACTION="<%$RT::WebPath%>/Admin/Queues/Modify.html" METHOD=POST>
+%if ($Create ) {
+<INPUT TYPE=HIDDEN NAME=id VALUE="new">
+% } else {
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$QueueObj->Id%>">
+% }
+
+<TABLE>
+<TR><TD ALIGN=RIGHT>
+Queue Name: 
+</TD>
+<TD><INPUT name="Name" value="<%$QueueObj->Name%>"></TD>
+</TR><TR>
+<TD ALIGN=RIGHT>
+Description:</TD><TD COLSPAN=3><INPUT name="Description" value="<%$QueueObj->Description%>" size=60></TD></TR>
+<TR>
+<TD ALIGN=RIGHT>
+Correspondence Address:
+</TD><TD>
+<INPUT name="CorrespondAddress" value="<%$QueueObj->CorrespondAddress%>">
+<BR><font size="-1"><i>(If left blank, will default to <%$RT::CorrespondAddress%></i></font>
+</TD>
+<TD ALIGN=RIGHT>
+
+Comment Address: </TD><TD>
+<INPUT NAME="CommentAddress" value="<%$QueueObj->CommentAddress%>">
+<BR><font size="-1"><i>(If left blank, will default to <%$RT::CommentAddress%></i></font>
+</TD>
+</TR><TR>
+
+<TD ALIGN=RIGHT>
+Priority starts at: 
+</TD><TD><INPUT NAME="InitialPriority" value="<%$QueueObj->InitialPriority %>">
+</TD>
+<TD ALIGN=RIGHT>
+Over time, priority moves toward:
+</TD><TD><INPUT NAME="FinalPriority" value="<%$QueueObj->FinalPriority %>">
+</TD>
+</TR>
+<TR>
+<TD ALIGN=RIGHT>
+Requests should be due in:
+</TD><TD>
+<INPUT NAME="DefaultDueIn" VALUE="<%$QueueObj->DefaultDueIn%>"> days.
+</TD>
+</TR>
+<TR>
+<TD>
+</TD>
+<TD COLSPAN=4><INPUT TYPE=HIDDEN NAME="SetEnabled" VALUE="1">
+<INPUT TYPE=CHECKBOX NAME="Enabled" VALUE="1" <%$EnabledChecked%>> Enabled (Unchecking this box disables this queue)<BR>
+</TD>
+</TR>
+
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit &>
+</form>
+
+
+
+<%INIT>
+
+my $QueueObj = new RT::Queue($session{'CurrentUser'});
+my  ($title, @results, $Disabled, $EnabledChecked);
+
+if ($Create) {
+    $title = "Create a queue";
+}
+
+else {
+    if ($id eq 'new') {
+       my ($val, $msg) =  $QueueObj->Create(Name => $Name);
+       if ($val == 0 ) {
+           Abort("Could not create queue: $msg");
+       }
+       else {
+               push @results, $msg;
+       }    
+     }
+     else {
+        $QueueObj->Load($id) || $QueueObj->Load($Name) || Abort("Couldn't load queue '$Name'");
+    }
+        $title = 'Editing Configuration for queue '.$QueueObj->Name;
+    
+}
+if ($QueueObj->Id()) {
+my @attribs= qw(Description CorrespondAddress CommentAddress Name 
+                InitialPriority FinalPriority DefaultDueIn);
+
+  @results = UpdateRecordObject( AttributesRef => \@attribs, 
+                                   Object => $QueueObj, 
+                                   ARGSRef => \%ARGS);
+
+}
+
+#we're asking about enabled on the web page but really care about disabled.
+if ($Enabled == 1) {
+    $Disabled = 0;
+}      
+else {
+    $Disabled = 1;
+}
+if  ( ($SetEnabled) and ( $Disabled != $QueueObj->Disabled) ) { 
+    my  ($code, $msg) = $QueueObj->SetDisabled($Disabled);
+    push @results, 'Enabled status '. $msg;
+}
+
+unless ($QueueObj->Disabled()) {
+    $EnabledChecked ="CHECKED";
+}
+</%INIT>
+
+
+<%ARGS>
+$id => undef
+$result => undef
+$Name => undef
+$Create => undef
+$Description => undef
+$CorrespondAddress => undef
+$CommentAddress => undef
+$InitialPriority => undef
+$FinalPriority => undef
+$DefaultDueIn => undef
+$SetEnabled => undef
+$Enabled => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Queues/People.html b/rt/webrt/Admin/Queues/People.html
new file mode 100755 (executable)
index 0000000..b495400
--- /dev/null
@@ -0,0 +1,161 @@
+<& /Elements/Header, Title => 'Modify people related to queue ' . $QueueObj->Name &>
+<& /Admin/Elements/QueueTabs, id => $id  &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<FORM METHOD=POST ACTION="People.html">
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$QueueObj->Id%>">
+<& /Elements/TitleBoxStart, title => 'Modify watchers for queue \''.$QueueObj->Name ."'",   width => "100%" &>
+
+<TABLE WIDTH=100%>
+<TR>
+<TD VALIGN=TOP >
+
+<h3>Current watchers</h3>
+<i>(Check box to delete)</i><br><BR>
+
+
+Cc:
+
+<ul>
+
+%# Print out a placeholder if there are none.
+%if ($cc->Count == 0 ) {
+<li><i>none</i>
+% }
+
+%while (my $watcher=$cc->Next) {
+<li>
+<INPUT TYPE=CHECKBOX NAME="DelWatcher<%$watcher->id%>" UNCHECKED>
+%# account
+%if ($watcher->IsUser) { 
+<a href="<%$RT::WebPath%>/Admin/Users/Modify.html?id=<%$watcher->OwnerObj->id%>">
+<%$watcher->OwnerObj->RealName%></a>:
+%} else {
+Email address:
+%}
+<i><%$watcher->Email%></i>
+%}
+</ul>
+
+
+Administrative Cc:
+<UL>
+%# Print out a placeholder if there are none.
+%if ($admincc->Count == 0 ) {
+<li><i>none</i>
+% }
+
+%while (my $watcher=$admincc->Next) {
+<li><INPUT TYPE=CHECKBOX NAME="DelWatcher<%$watcher->id%>" UNCHECKED>
+%# account
+%if ($watcher->IsUser) { 
+<a href="<%$RT::WebPath%>/Admin/Users/Modify.html?id=<%$watcher->OwnerObj->id%>">
+<%$watcher->OwnerObj->RealName%></a>:
+%} else {
+Email address:
+%}
+<i><%$watcher->Email%></i>
+%}
+</UL>
+</TD>
+
+<TD VALIGN=TOP>
+<h3>New watchers</h3>
+Find people whose<BR>
+<& /Elements/SelectUsers &>
+
+<BR>
+Add new watchers:<br>
+
+% if ($msg) {
+<i><%$msg%></i>
+% } elsif ($Users) {
+<ul>
+% while (my $u = $Users->Next ) {
+<li><&/Elements/SelectWatcherType, Scope=>'queue', Name => "WatcherTypeUser".$u->Id &> <%$u->Name%>
+(<%$u->RealName%>)
+% }
+</ul>
+% }
+
+</TD>
+</TR>
+</TABLE>
+
+
+
+
+
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit, Label => 'Save Changes', Caption => "If you've updated anything above, be sure to" &>
+</form>
+
+<%INIT>
+
+my ($field, @results, $User, $Users, $watcher, $key, $msg);
+# {{{ Load the queue
+#If we get handed two ids, mason will make them an array. bleck.
+# We want teh first one. Just because there's no other sensible way
+# to deal
+
+
+
+my $QueueObj = new RT::Queue($session{'CurrentUser'});
+$QueueObj->Load($id) || Abort("Couldn't load queue '$id'");
+# }}}
+
+# {{{ Delete deletable watchers
+
+foreach $key (keys %ARGS) {
+    if (($key =~ /^DelWatcher(\d*)$/) and
+       ($ARGS{$key})) {
+       $RT::Logger->debug("Deleting watcher $1\n");
+       my ($code, $msg) = $QueueObj->DeleteWatcher($1);
+       
+       push @results, $msg;
+    }
+}
+# }}}
+
+# {{{ Add new watchers
+foreach $key (keys %ARGS) {
+    #They're in this order because otherwise $1 gets clobbered :/
+    if ( ($ARGS{$key} =~ /^(AdminCc|Cc)$/) and
+        ($key =~ /^WatcherTypeUser(\d*)$/) ) {
+       $RT::Logger->debug("Adding a watcher $1 to ".$ARGS{$key}."\n");
+       my ($code, $msg) = 
+         $QueueObj->AddWatcher(Type => $ARGS{$key},
+                                Owner => $1);
+       push @results, $msg;
+    }
+}
+
+# }}}
+
+
+
+my $admincc = $QueueObj->AdminCc;
+my $cc = $QueueObj->Cc;
+
+if (!$ARGS{'UserString'}) {
+$msg = "No users selected.";
+ }
+else {
+    $Users = new RT::Users($session{'CurrentUser'});
+    $Users->Limit(FIELD => $ARGS{'UserField'},
+                 VALUE => $ARGS{'UserString'},
+                 OPERATOR => $ARGS{'UserOp'});
+     }
+</%INIT>
+
+<%ARGS>
+$UserField => 'Name'
+$UserOp => '='
+$UserString => undef
+$Type => undef
+$id => undef
+</%ARGS>
+
diff --git a/rt/webrt/Admin/Queues/Scrips.html b/rt/webrt/Admin/Queues/Scrips.html
new file mode 100755 (executable)
index 0000000..95b8c43
--- /dev/null
@@ -0,0 +1,111 @@
+<& /Admin/Elements/Header, Title => 'Edit scrips' &>
+<& /Admin/Elements/QueueTabs, id => $QueueObj->Id &>
+
+<& /Elements/ListActions, actions => \@actions &>
+
+<& /Elements/TitleBoxStart, title => $description &>
+<h2>Global Scrips</h2>
+<& /Admin/Elements/ListGlobalScrips &>
+<BR> 
+  <FORM METHOD=POST ACTION="Scrips.html">
+    <INPUT TYPE=HIDDEN NAME=id VALUE=<%$id%>>
+<h2>Queue Scrips</h2>
+% if ($Scrips->Count > 0 ) {
+<TABLE>
+<TR>
+<TD>Delete
+</TD>
+<TD>
+</TR>
+% while (my $scrip = $Scrips->Next ) {
+<TR>
+<TD>
+<INPUT TYPE="CHECKBOX" NAME="DeleteScrip-<%$scrip->Id%>">
+</TD>
+<TD>
+<% $scrip->ConditionObj->Name %> 
+<% $scrip->ActionObj->Name %> with template
+<% $scrip->TemplateObj->Name %>
+</TD>
+</TR>
+% }
+</TABLE>
+% }
+<BR>
+<h2>Add a scrip to this queue</h2>
+Condition: <& /Admin/Elements/SelectScripCondition, Name => 'NewScripCondition' &>
+         Action: <& /Admin/Elements/SelectScripAction, Name => 'NewScripAction' &>
+         Template: <& /Admin/Elements/SelectTemplate, Name => 'NewScripTemplate', DefaultQueue => $id &>
+
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit &>
+</FORM>
+<%init>
+my (@actions, $description);
+
+my $Scrips = new RT::Scrips ($session{'CurrentUser'});
+unless ($id =~ /^\d+$/) {
+    Abort("$id isn't a valid Queue id.");
+}
+
+unless ($Scrips->LimitToQueue($id)) {
+    Abort("Couldn't load Scrips.");
+        }
+
+my $QueueObj = new RT::Queue($session{'CurrentUser'});
+$QueueObj->Load($id);
+
+if ($QueueObj->id) {
+    $description = "Modify scrips for queue '". $QueueObj->Name ."'";
+}
+else {
+    $description = "Modify global scrips";
+}
+
+
+if ($NewScripAction and $NewScripCondition) {
+    my $NewScrip = new RT::Scrip($session{'CurrentUser'});
+    
+    my ($retval, $msg) = $NewScrip->Create ( ScripAction => $NewScripAction,
+                                    ScripCondition => $NewScripCondition,
+                                    Stage => 'TransactionCreate',
+                                    Queue => $id,
+                                    Template => $NewScripTemplate);
+    if (defined $retval) {
+        push @actions, $msg;
+    }
+    else {
+        push @actions, $msg;
+    }
+}
+
+# {{{ deal with modifying and deleting existing scrips
+my ($key );
+foreach $key (keys %ARGS) {
+       # {{{ if we're trying to delete the scrip
+    if ($key =~ /^DeleteScrip-(\d+)/) {
+            my $id = $1;
+               my $scrip = new RT::Scrip($session{'CurrentUser'});
+               $scrip->Load($id);
+           my ($retval, $msg) = $scrip->Delete;
+           if ($retval) {
+               push @actions, 'Scrip deleted';
+           }
+           else {
+               push @actions, $msg;
+           }
+       }
+       # }}}
+
+    
+}
+# }}}
+</%init>
+
+<%ARGS>
+$NewScripCondition => undef
+$NewScripAction => undef
+$NewScripTemplate => undef
+$id => undef         #some identifier that a Queue could 
+</%ARGS>
diff --git a/rt/webrt/Admin/Queues/Template.html b/rt/webrt/Admin/Queues/Template.html
new file mode 100755 (executable)
index 0000000..61ee418
--- /dev/null
@@ -0,0 +1,68 @@
+<& /Admin/Elements/Header, title => "Modify template ".$TemplateObj->id&>
+<& /Admin/Elements/QueueTabs, id => $Queue &>
+<& /Elements/ListActions, actions => \@results &>
+
+<& /Elements/TitleBoxStart, title => $title &>
+
+<FORM METHOD=POST ACTION="Template.html">
+%if ($create ) {
+<INPUT TYPE=HIDDEN NAME=template VALUE="new">
+% } else {
+<INPUT TYPE=HIDDEN NAME=template VALUE="<%$TemplateObj->Id%>">
+% }
+
+%# hang onto the queue id
+<INPUT TYPE=HIDDEN name="Queue" value="<%$Queue%>">
+
+
+Name: <input name="Name" VALUE="<%$TemplateObj->Name%>" SIZE=20><BR>
+Description: <input name="Description" VALUE="<%$TemplateObj->Description%>" SIZE=80><BR>
+
+<TEXTAREA NAME=Content ROWS=25 COLS=80 WRAP=SOFT>
+<%$TemplateObj->Content%></TEXTAREA>
+
+<& /Elements/TitleBoxEnd&>
+<&/Elements/Submit&>
+</FORM>
+
+
+
+<%INIT>
+
+my $TemplateObj = new RT::Template($session{'CurrentUser'});
+my  ($title, @results);
+
+if ($create) {
+  $title = "Create a template";
+}
+
+else {
+  if ($template eq 'new') {
+      my ($val, $msg) =  $TemplateObj->Create(Queue => $Queue, Name => $Name);
+      Abort("Could not create template: $msg") unless ($val);
+     push @results, $msg;
+     $title = 'Created template ' . $TemplateObj->Name(); 
+    }
+    else {
+       $TemplateObj->Load($template) || Abort('No Template');
+      $title = 'Editing template ' . $TemplateObj->Name(); 
+    }
+  
+    
+}
+if ($TemplateObj->Id()) {
+  $Queue = $TemplateObj->Queue;
+
+  my @attribs = qw( Description Content Queue Name);
+  my @aresults = UpdateRecordObject( AttributesRef => \@attribs, 
+                                    Object => $TemplateObj, 
+                                    ARGSRef => \%ARGS);
+  push @results, @aresults;
+}
+</%INIT>
+<%ARGS>
+$Queue => undef
+$template => undef
+$create => undef
+$Name => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Queues/Templates.html b/rt/webrt/Admin/Queues/Templates.html
new file mode 100755 (executable)
index 0000000..218d41d
--- /dev/null
@@ -0,0 +1,24 @@
+<& /Admin/Elements/Header, Title => 'Edit templates for '.$Queue->Name &>
+<& /Admin/Elements/QueueTabs, id => $Queue->id &>
+
+<& /Elements/TitleBoxStart, title => 'Edit templates for '.$Queue->Name &>
+<UL>
+<LI><A href="Template.html?create=1&Queue=<%$Queue->id%>">Create a new template</A><BR><BR>
+
+%while  (my $TemplateObj = $Templates->Next) { 
+
+<LI><A HREF="Template.html?Queue=<%$id%>&template=<%$TemplateObj->id()%>"><%$TemplateObj->id()%>/<%$TemplateObj->Name%>: <%$TemplateObj->Description%></a><BR>
+
+%}
+
+<& /Elements/TitleBoxEnd &>
+<%INIT>
+
+my $Queue = new RT::Queue($session{'CurrentUser'});
+$Queue->Load($id);
+my $Templates = $Queue->Templates;
+
+</%INIT>
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Queues/UserRights.html b/rt/webrt/Admin/Queues/UserRights.html
new file mode 100755 (executable)
index 0000000..75d9cb2
--- /dev/null
@@ -0,0 +1,72 @@
+<& /Admin/Elements/Header, Title => 'Modify user rights for queue '. $QueueObj->Name &>
+<& /Admin/Elements/QueueTabs, id => $id &>  
+<& /Elements/ListActions, actions => \@results &>
+
+  <FORM METHOD=POST ACTION="UserRights.html">
+    <INPUT TYPE=HIDDEN NAME=id VALUE="<% $QueueObj->id %>">
+      
+<& /Elements/TitleBoxStart, title => 'Modify user rights for queue '.$QueueObj->Name &>
+      
+<TABLE>
+        
+%      while (my $UserObj = $Users->Next()) {
+  <TR ALIGN=RIGHT> 
+       <TD VALIGN=TOP>
+           <% $UserObj->Name %>
+                 </TD>
+         <TD>
+           <& /Admin/Elements/SelectRights, PrincipalObj => $UserObj,
+                   PrincipalType => 'User',
+                     Scope => 'Queue',
+                       QueueObj => $QueueObj &>
+         </TD>
+       </TR>
+% }
+      </TABLE>
+            
+      <& /Elements/TitleBoxEnd &>
+      <& /Elements/Submit, Caption => "Be sure to save your changes", Reset => 1 &>
+      
+  </FORM>
+  
+<%INIT>
+  #Update the acls.
+  my @results =  ProcessACLChanges(\@CheckACL, \%ARGS);
+
+# {{{ Deal with setting up the display of current rights.
+
+# {{{ do basic initialization.
+
+#Define vars used in html above
+my ($GroupObj);
+
+my ($right);
+
+
+if (!defined $id) {
+    Abort("No Queue defined");
+}
+
+my $QueueObj = new RT::Queue($session{'CurrentUser'});
+$QueueObj->Load($id) ||
+  Abort("Couldn't load queue $id");
+
+# Find out which users we want to display ACL selects for
+my $Users = new RT::Users($session{'CurrentUser'});
+$Users->LimitToPrivileged();
+
+# }}}
+    
+  
+# }}}
+    
+</%INIT>
+
+<%ARGS>
+$id => undef
+$UserString => undef
+$UserOp => undef
+$UserField => undef
+@CheckACL => undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Queues/index.html b/rt/webrt/Admin/Queues/index.html
new file mode 100755 (executable)
index 0000000..52dfb73
--- /dev/null
@@ -0,0 +1,52 @@
+<& /Admin/Elements/Header, Title => 'Admin queues' &>
+<& /Admin/Elements/Tabs, current_tab => 'Admin/Queues/' &>
+
+
+<& /Elements/TitleBoxStart, title => 'Select a queue' &>
+
+<TABLE>
+<TR>
+<TD VALIGN=TOP>
+
+<FORM METHOD=POST ACTION="<% $RT::WebPath %>/Admin/Queues/">
+
+<input type="checkbox" name="FindDisabledQueues"> Include disabled queues in listing.
+<BR>
+<div align=right><input type=submit value="Go!"></div> 
+</FORM>
+</TD>
+<TD VALIGN=TOP>
+<UL>
+% if ($session{'CurrentUser'}->HasSystemRight('AdminQueue')) {
+<LI><A HREF="<%$RT::WebPath%>/Admin/Queues/Modify.html?Create=1">Create a new queue</A><BR><BR></LI>
+</UL>
+% }
+
+<%$caption%><BR>
+<UL>
+%if ($queues->Count == 0) {
+<LI> <i>No queues matching search criteria found.</i>
+% }
+%while ( $queue = $queues->Next) {
+<LI><A HREF="Modify.html?id=<%$queue->id%>"><%$queue->Name%></a></LI>
+%}
+
+</UL>
+</TD>
+</TR>
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+
+<%INIT>
+my ($queue, $caption);
+my $queues = new RT::Queues($session{'CurrentUser'});
+$queues->UnLimit();
+
+if ($FindDisabledQueues) {
+       $queues->{'find_disabled_rows'} = 1;
+}
+
+</%INIT>
+<%ARGS>
+$FindDisabledQueues => 0
+</%ARGS>
diff --git a/rt/webrt/Admin/Users/Modify.html b/rt/webrt/Admin/Users/Modify.html
new file mode 100755 (executable)
index 0000000..b6daed4
--- /dev/null
@@ -0,0 +1,259 @@
+<& /Admin/Elements/Header, Title => $title &>
+<& /Admin/Elements/UserTabs, id => $id, current_subtab => '/Admin/Elements/Modify.html?id='.$id &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<FORM ACTION="<%$RT::WebPath%>/Admin/Users/Modify.html" METHOD=POST>
+%if ($Create) {
+<INPUT TYPE=HIDDEN NAME=id VALUE="new">
+% } else {
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$UserObj->Id%>">
+% }
+
+<TABLE WIDTH=100% BORDER=0>
+<TR>
+
+<TD VALIGN=TOP ROWSPAN=2>
+<& /Elements/TitleBoxStart, title => 'Identity' &>
+
+Username: <input name="Name" value="<%$UserObj->Name%>"> <b>(required)</b> <BR>
+Email: <input name="EmailAddress" value="<%$UserObj->EmailAddress%>"><BR>
+Real Name: <input name="RealName" value="<%$UserObj->RealName%>"> <BR>
+Nickname: <input name="NickName" value="<%$UserObj->NickName%>">
+<BR>
+Unix login: <input name="Gecos" value="<%$UserObj->Gecos%>">
+<BR>
+Extra info: <textarea name="FreeformContactInfo" cols=20 rows=5><%$UserObj->FreeformContactInfo%></TEXTAREA>
+<& /Elements/TitleBoxEnd &>
+</TD>
+<TD VALIGN=TOP>
+<& /Elements/TitleBoxStart, title => 'Access control' &>
+<INPUT TYPE=HIDDEN NAME="SetEnabled" VALUE="1">
+<INPUT TYPE=CHECKBOX NAME="Enabled" VALUE="1" <%$EnabledChecked%>>
+Let this user access RT<BR>
+
+
+<INPUT TYPE=HIDDEN NAME="SetPrivileged" VALUE="1">
+<INPUT TYPE=CHECKBOX NAME="Privileged" VALUE="1" <%$PrivilegedChecked%>> Let this user be granted rights<BR>
+                   
+% unless ($RT::WebExternalAuth) {
+<TABLE>
+<TR>
+<TD ALIGN=RIGHT>
+New Password:
+</TD>
+<TD ALIGN=LEFT>
+<input type=password name="Pass1">
+</TD>
+</TR>
+<TR><TD ALIGN=RIGHT>
+Retype Password:
+</TD>
+<TD>
+<input type=password name="Pass2">
+</TD>
+</TR>
+</TABLE>
+% }
+<& /Elements/TitleBoxEnd &>
+</TD>
+<TR>
+
+<TD VALIGN=TOP>
+<& /Elements/TitleBoxStart, title => 'Location' &>
+Organization: <input name="Organization" value="<%$UserObj->Organization%>">
+<BR>
+Address1: <input name="Address1" value="<%$UserObj->Address1%>">
+<BR>
+Address2: <input name="Address2" value="<%$UserObj->Address2%>">
+<BR>
+City: <input name="City" value="<%$UserObj->City%>" size=14>
+
+State: <input name="State" value="<%$UserObj->State%>" size=3>
+
+Zip: <input name="Zip" value="<%$UserObj->Zip%>" size=9>
+<BR>
+Country: <input name="Country" value="<%$UserObj->Country%>">
+<BR>
+
+
+<& /Elements/TitleBoxEnd &>
+</TD>
+</TR>
+<TR>
+<TD COLSPAN=2 VALIGN=TOP>
+
+
+<& /Elements/TitleBoxStart, title => 'Phone numbers' &>
+Home: <input name="HomePhone" value="<%$UserObj->HomePhone%>" size=13>
+
+Work: <input name="WorkPhone" value="<%$UserObj->WorkPhone%>" size=13>
+
+Mobile: <input name="MobilePhone" value="<%$UserObj->MobilePhone%>" size=13>
+
+Pager: <input name="PagerPhone" value="<%$UserObj->PagerPhone%>" size=13>
+<& /Elements/TitleBoxEnd &>
+<BR>
+<& /Elements/TitleBoxStart, title => 'Comments about this user' &>
+<TEXTAREA name="Comments" COLS=80 ROWS=5 WRAP=VIRTUAL><%$UserObj->Comments%></TEXTAREA>
+<& /Elements/TitleBoxEnd &>
+
+
+%if ($UserObj->Privileged) {
+<BR>
+<& /Elements/TitleBoxStart, title => 'Signature' &>
+<TEXTAREA COLS=80 ROWS=5 name="Signature" WRAP=HARD>
+<%$UserObj->Signature%></TEXTAREA>
+<& /Elements/TitleBoxEnd &>
+% }
+
+</TD>
+
+</TR>
+</TABLE>
+
+
+<& /Elements/Submit &>
+</form>
+
+
+<%INIT>
+
+my $UserObj = new RT::User($session{'CurrentUser'});
+my ($title, $PrivilegedChecked, $EnabledChecked, $Disabled, $result, @results);
+
+my ($val, $msg);
+
+if ($Create) {
+    $title = "Create a new user";
+} 
+else {
+
+    if ($id eq 'new') {
+       ($val, $msg) = $UserObj->Create( Name => $Name,
+                                        EmailAddress => $ARGS{'EmailAddress'}
+                                      );
+       if ($val) {
+               push @results, $msg;
+       } else {
+               push @results, 'User could not be created: '. $msg;
+       }       
+       
+    }
+    else {
+       $UserObj->Load($id) || $UserObj->Load($Name) || Abort("Couldn't load user '$Name'");
+       $val = $UserObj->Id();
+    }
+
+    if ($val) {
+       $title = "Modify the user ". $UserObj->Name;
+    }  
+
+    # If the create failed
+    else {
+       $title = "Create a new user";
+       $Create = 1;
+    }    
+
+    
+
+}
+
+
+
+
+# If we have a user to modify, lets try. 
+if ($UserObj->Id) {
+    
+    my @fields = qw(Name Comments Signature EmailAddress FreeformContactInfo 
+                   Organization RealName NickName Lang EmailEncoding WebEncoding 
+                   ExternalContactInfoId ContactInfoSystem Gecos ExternalAuthId 
+                   AuthSystem HomePhone WorkPhone MobilePhone PagerPhone Address1
+               Address2 City State Zip Country 
+                  );
+    
+    my @fieldresults = UpdateRecordObject ( AttributesRef => \@fields,
+                                           Object => $UserObj,
+                                           ARGSRef => \%ARGS );
+    push (@results,@fieldresults);
+
+
+# {{{ Deal with special fields: Privileged, Enabled and Password
+if  ( ($SetPrivileged) and ( $Privileged != $UserObj->Privileged) ) {
+my  ($code, $msg) = $UserObj->SetPrivileged($Privileged);
+     push @results, 'Privileged status: '. $msg;
+}
+
+#we're asking about enabled on the web page but really care about disabled.
+if ($Enabled == 1) {
+    $Disabled = 0;
+}      
+else {
+    $Disabled = 1;
+}
+if  ( ($SetEnabled) and ( $Disabled != $UserObj->Disabled) ) { 
+    my  ($code, $msg) = $UserObj->SetDisabled($Disabled);
+    push @results, 'Enabled status '. $msg;
+}
+
+
+#TODO: make this report errors properly
+if ((defined $Pass1) and ($Pass1 ne '') and ($Pass1 eq $Pass2) and (!$UserObj->IsPassword($Pass1))) {
+    my ($code, $msg);
+    ($code, $msg) = $UserObj->SetPassword($Pass1);
+    push @results, 'Password: '. $msg;
+}
+
+# }}}
+}
+
+
+# {{{ Do some setup for the ui
+unless ($UserObj->Disabled()) {
+    $EnabledChecked ="CHECKED";
+}
+
+if ($UserObj->Privileged()) {  
+    $PrivilegedChecked = "CHECKED";
+}
+
+# }}}
+</%INIT>
+
+
+<%ARGS>
+$id => undef
+$Name  => undef
+$Comments  => undef
+$Signature  => undef
+$EmailAddress  => undef
+$FreeformContactInfo => undef
+$Organization  => undef
+$RealName  => undef
+$NickName  => undef
+$Privileged => undef
+$SetPrivileged => undef
+$Enabled => undef
+$SetEnabled => undef
+$Lang  => undef
+$EmailEncoding  => undef
+$WebEncoding => undef
+$ExternalContactInfoId  => undef
+$ContactInfoSystem  => undef
+$Gecos => undef
+$ExternalAuthId  => undef
+$AuthSystem  => undef
+$HomePhone => undef
+$WorkPhone  => undef
+$MobilePhone  => undef
+$PagerPhone  => undef
+$Address1 => undef
+$Address2  => undef
+$City  => undef
+$State  => undef
+$Zip  => undef
+$Country => undef
+$Pass1 => undef
+$Pass2=> undef
+$Create=> undef
+</%ARGS>
diff --git a/rt/webrt/Admin/Users/Prefs.html b/rt/webrt/Admin/Users/Prefs.html
new file mode 100755 (executable)
index 0000000..4a9fc5c
--- /dev/null
@@ -0,0 +1,97 @@
+<& /Elements/Header, Title=>"User view" &>
+
+<& /Elements/ViewUser, User=>$u &>
+
+%if ($session{CurrentUser} && ($session{CurrentUser}->Id == $id)) {
+       <& /Elements/TitleBoxStart, title => 'Signature'  &>
+<form method=post>
+<input type="hidden" name="id" value=<%$id%>>
+<TEXTAREA COLS=72 ROWS=4 WRAP=HARD NAME="Signature"><% $u->Signature %></TEXTAREA><br><br>
+<input type="submit" value="Update signature">
+</form>
+         <& /Elements/TitleBoxEnd &>
+         <form method=post>
+         Open tickets (from listing) in another window: <input type="checkbox" name="NewWindowOption" <%exists $session{NewWindowOption} && "CHECKED"%>><br>
+         Open tickets (from listing) in a new window: <input type="checkbox" name="AlwaysNewWindowOption" <%exists $session{AlwaysNewWindowOption} && "CHECKED"%>><br>
+         <input type="submit" name="NewWindowSetting" value="New window setting">
+         </form>
+%}
+
+       <& /Elements/TitleBoxStart, title => 'Email'  &>
+<form method=post>
+<input type="hidden" name="id" value="<%$id%>">
+<input name="Email" value="<% $u->EmailAddress %>"><input type="submit" value="Update email">
+</form>
+         <& /Elements/TitleBoxEnd &>
+       <& /Elements/TitleBoxStart, title => 'Real Name'  &>
+<form method=post>
+<input type="hidden" name="id" value="<%$id%>">
+<input name="RealName" value="<% $u->RealName %>"><input type="submit" value="Update name">
+</form>
+         <& /Elements/TitleBoxEnd &>
+
+       <& /Elements/TitleBoxStart, title => 'User ID'  &>
+<form method=post>
+<input type="hidden" name="id" value="<%$id%>">
+<input name="Name" value="<% $u->Name %>"><input type="submit" value="Update ID">
+</form>
+         <& /Elements/TitleBoxEnd &>
+
+%# TODO: alternative email addresses + merging users
+
+<%ARGS>
+$id => $session{CurrentUser} ? $session{CurrentUser}->Id : 0
+$Signature => undef
+$Email => undef
+$RealName => undef
+$Name => undef
+</%ARGS>
+
+<%INIT>
+require RT::User;
+my $u=RT::User->new($session{CurrentUser});
+$u->Load($id) || die "Couldn't load that user ($id)";
+if ($Signature) {
+my ($val, $msg)=$u->SetSignature($Signature);
+$RT::Logger->log(level=>($val ? 'info' : 'error'), message=>$msg);
+}
+
+if ($Email) {
+my ($val, $msg)=$u->SetEmailAddress($Email);
+$RT::Logger->log(level=>($val ? 'info' : 'error'), message=>$msg);
+}
+
+if ($RealName) {
+my ($val, $msg)=$u->SetRealName($RealName);
+$RT::Logger->log(level=>($val ? 'info' : 'error'), message=>$msg);
+}
+
+if ($Name) {
+my ($val, $msg)=$u->SetName($Name);
+$RT::Logger->log(level=>($val ? 'info' : 'error'), message=>$msg);
+}
+
+if ($ARGS{NewWindowSetting}) {
+if ($ARGS{NewWindowOption}) {
+$session{NewWindowOption}=1;
+} else {
+delete $session{NewWindowOption};
+}
+if ($ARGS{AlwaysNewWindowOption}) {
+$session{NewWindowOption}=1;
+$session{AlwaysNewWindowOption}=1;
+} else {
+delete $session{AlwaysNewWindowOption};
+}
+}
+
+</%INIT>
+
+
+
+
+
+
+
+
+
diff --git a/rt/webrt/Admin/Users/Rights.html b/rt/webrt/Admin/Users/Rights.html
new file mode 100644 (file)
index 0000000..3b94f91
--- /dev/null
@@ -0,0 +1 @@
+Placeholder
diff --git a/rt/webrt/Admin/Users/index.html b/rt/webrt/Admin/Users/index.html
new file mode 100755 (executable)
index 0000000..3835137
--- /dev/null
@@ -0,0 +1,71 @@
+<& /Admin/Elements/Header, Title => 'Admin users' &>
+<& /Admin/Elements/Tabs, current_tab => 'Admin/Users/' &>
+
+
+<& /Elements/TitleBoxStart, title => 'Select a user' &>
+
+<TABLE>
+<TR>
+<TD VALIGN=TOP>
+
+<FORM METHOD=POST ACTION="<% $RT::WebPath %>/Admin/Users/">
+
+Find people whose <& /Elements/SelectUsers &><BR>
+<input type="checkbox" name="FindDisabledUsers"> Include disabled users in search.
+<BR>
+<div align=right><input type=submit value="Go!"></div> 
+</FORM>
+</TD>
+<TD VALIGN=TOP>
+<UL>
+% if ($session{'CurrentUser'}->HasSystemRight('AdminUsers')) {
+<LI><A HREF="<%$RT::WebPath%>/Admin/Users/Modify.html?Create=1">Create a new user</A><BR><BR></LI>
+</UL>
+% }
+
+<%$caption%><BR>
+<UL>
+%if ($users->Count == 0) {
+<LI> <i>No users matching search criteria found.</i>
+% }
+%while ( $user = $users->Next) {
+<LI><A HREF="Modify.html?id=<%$user->id%>"><%$user->Name || '(no name listed)'%></a></LI>
+%}
+
+</UL>
+</TD>
+</TR>
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+
+<%INIT>
+my ($user, $caption);
+my $users = new RT::Users($session{'CurrentUser'});
+
+if ($FindDisabledUsers) {
+       $users->{'find_disabled_rows'} = 1;
+}
+
+unless (defined $UserString) {
+    $users->LimitToPrivileged();
+    $caption = "Privileged users";
+}
+else {
+    $caption = "Users matching search criteria";
+
+  if ($UserString) {
+       $users->Limit( FIELD => $UserField,
+                       OPERATOR => $UserOp,
+                      VALUE => $UserString); 
+
+}
+}
+</%INIT>
+<%ARGS>
+$UserString => undef
+$UserOp => '='
+$UserField => 'Name'
+$IdLike => undef
+$EmailLike => undef
+$FindDisabledUsers => 0
+</%ARGS>
diff --git a/rt/webrt/Admin/index.html b/rt/webrt/Admin/index.html
new file mode 100755 (executable)
index 0000000..1ed973f
--- /dev/null
@@ -0,0 +1,4 @@
+  <& /Admin/Elements/Header, Title => 'RT Administration' &>
+<& /Admin/Elements/Tabs &>
+
+
diff --git a/rt/webrt/Elements/Checkbox b/rt/webrt/Elements/Checkbox
new file mode 100755 (executable)
index 0000000..964c482
--- /dev/null
@@ -0,0 +1,17 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/Checkbox,v 1.1 2002-08-12 06:17:08 ivan Exp $
+<INPUT TYPE="Checkbox" NAME ="<%$Name%>" <%$IsChecked%>>
+
+<%ARGS>
+$Name => undef
+$Default => undef
+$True => undef
+$False => undef
+$IsChecked => undef
+</%ARGS>
+
+<%INIT>
+$IsChecked = 
+  ($Default && $Default =~ /checked/i)
+    ? " CHECKED " : "";
+1;
+</%INIT>
diff --git a/rt/webrt/Elements/CreateTicket b/rt/webrt/Elements/CreateTicket
new file mode 100644 (file)
index 0000000..1270f6e
--- /dev/null
@@ -0,0 +1 @@
+<FORM ACTION="<% $RT::WebPath%>/Ticket/Create.html"><input type=submit value="New ticket in">&nbsp;<& /Elements/SelectNewTicketQueue &></FORM>
diff --git a/rt/webrt/Elements/CustomHomepageHeader b/rt/webrt/Elements/CustomHomepageHeader
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/rt/webrt/Elements/Error b/rt/webrt/Elements/Error
new file mode 100755 (executable)
index 0000000..ec2cf51
--- /dev/null
@@ -0,0 +1,23 @@
+<& /Elements/Header, Code => $Code, Why => $Why &>
+<& /Elements/Tabs &>
+<& /Elements/TitleBoxStart, title => $Title &>
+<%$Why%>
+<br>
+<font size=-1>
+<%$Details%>
+</font>
+<& /Elements/TitleBoxEnd &>
+</body>
+</HTML>
+
+
+<%args>
+$Code => undef
+$Details => undef
+$Title => "RT Error"
+$Why => "the calling component did not specify why"
+</%args>
+
+<%INIT>
+$RT::Logger->error("WebRT: $Why ($Details)");
+</%INIT>
diff --git a/rt/webrt/Elements/Footer b/rt/webrt/Elements/Footer
new file mode 100755 (executable)
index 0000000..776c219
--- /dev/null
@@ -0,0 +1,10 @@
+% if ($Debug) {
+<HR>
+<b>Time to display: <%time - $m->{'rt_base_time'} %></b>
+% }
+</BODY>
+</HTML>
+
+<%ARGS>
+$Debug => 0
+</%ARGS>
diff --git a/rt/webrt/Elements/GotoTicket b/rt/webrt/Elements/GotoTicket
new file mode 100644 (file)
index 0000000..21d2bcd
--- /dev/null
@@ -0,0 +1 @@
+<FORM ACTION="<%$RT::WebPath%>/Ticket/Display.html"><input type=submit value="Goto ticket">&nbsp;<input size=5 name=id></FORM>
diff --git a/rt/webrt/Elements/Header b/rt/webrt/Elements/Header
new file mode 100755 (executable)
index 0000000..471331b
--- /dev/null
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<HTML>
+<HEAD>
+<TITLE><%$Title%></TITLE>
+<META HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
+
+%# TODO this gets called from error. but I have no idea what it might
+%# be used for. can we whack it?  -jesse
+% if ($Code) {
+<META HTTP-EQUIV VALUE="<%$Code%> <%$Why%>">
+% }
+% if ($Refresh > 0) {
+<META HTTP-EQUIV="REFRESH" CONTENT="<%$Refresh%>">
+% }
+
+<link rel="stylesheet" href="<%$RT::WebPath%>/NoAuth/webrt.css" type="text/css">
+</HEAD>
+<BODY BGCOLOR="<%$BgColor%>">
+% if ($ShowBar) {
+<TABLE BORDER=0 WIDTH=100% CELLSPACING=0 BGCOLOR="#993333">
+<TR VALIGN=TOP>
+<TD><IMG SRC="<%$RT::LogoURL%>" alt="RT"></TD>
+<TD VALIGN=CENTER ALIGN=LEFT>
+<font size=+2 color="#ffffff">
+<B>
+<%$Title%>
+</B>
+</font>
+</TD>
+<TD ALIGN=RIGHT>
+<font color="#ffffff">
+% if ($session{'CurrentUser'}) {
+Signed in as <b><%$session{'CurrentUser'}->Name%></b>.<BR>
+% if ($session{'CurrentUser'}->HasSystemRight('ModifySelf')) {
+[<A class='inverse' HREF="<%$RT::WebPath%>/User/Prefs.html" >Preferences</A>] 
+% }
+% unless ($RT::WebExternalAuth) { 
+[<A class='inverse' HREF="<%$RT::WebPath%>/NoAuth/Logout.html">Logout</a>]
+% }
+% } else {
+Not logged in.
+% }
+</font>
+</TD>
+</TR>
+</TABLE>
+
+<BR>
+% }
+<%ARGS>
+$Title => 'WebRT'
+$Code => undef
+$Refresh => undef
+$Why => undef
+$BgColor => '#ffffff'
+$ShowBar => 1
+</%ARGS>
+<%INIT>
+$Title = "RT/$RT::rtname: ".$Title;
+</%INIT>
+
diff --git a/rt/webrt/Elements/ListActions b/rt/webrt/Elements/ListActions
new file mode 100755 (executable)
index 0000000..3fc9b0b
--- /dev/null
@@ -0,0 +1,14 @@
+% if (@actions ) {
+<& /Elements/TitleBoxStart, title => 'Results' &>
+<UL>
+% foreach my $action (@actions) {
+% next unless ($action);
+<LI><%$action%></LI>
+% }
+</UL>
+<& /Elements/TitleBoxEnd &>
+<BR>
+% }
+<%ARGS>
+@actions => undef
+</%ARGS>
diff --git a/rt/webrt/Elements/Login b/rt/webrt/Elements/Login
new file mode 100755 (executable)
index 0000000..27ec982
--- /dev/null
@@ -0,0 +1,69 @@
+<& /Elements/Header, Title=>"Login" , &>
+
+<DIV ALIGN=CENTER>
+% if ($Error) {
+<& /Elements/TitleBoxStart, title => 'Error' &>
+<% $Error %>
+<& /Elements/TitleBoxEnd &>
+% }
+<BR>
+<& /Elements/TitleBoxStart, width=> "40%", titleright => "RT $RT::VERSION for $RT::rtname", title => 'Login' ,
+contentbg=>"#cccccc" &>
+
+
+% unless ($RT::WebExternalAuth) {
+<FORM METHOD=POST >
+<TABLE BORDER=0 WIDTH=100%>
+<TR ALIGN=RIGHT>
+<TD ALIGN=RIGHT>Username:</TD><TD ALIGN=LEFT><input name=user value="<%$user%>"></TD></TR>
+<TR><TD ALIGN=RIGHT>Password:</TD><TD ALIGN=LEFT><input type=password name=pass></TD></TR>
+<TR><TD colspan=2 align=right>
+<input type=submit Value="Login">
+</TD></TR>
+</TABLE>
+<&/Elements/TitleBoxEnd&>
+% # From mason 1.0.1 forward, this doesn't work. in fact, it breaks things.
+% if (0) {
+% # The code below iterates through everything in the passed in arguments
+% # Preserving all the old parameters
+% # This would be easier, except mason is 'smart' and calls multiple values
+% # arrays rather than multiple hash keys
+% my $key; my $val;
+% foreach $key (keys %ARGS) {
+%  if (($key ne 'user') and ($key ne 'pass')) {
+%      if (ref($ARGS{$key}) =~ /ARRAY/) {
+%              foreach $val (@{$ARGS{$key}}) {
+<input type=hidden name="<%$key %>" value="<% $val %>">
+%              }
+%      }
+%      else {
+<input type="hidden" name="<% $key %>" value="<% $ARGS{$key} %>">
+%      }
+% }
+%}
+% }
+</FORM>
+% }
+</DIV>
+
+<BR>
+<!-- TODO: not yet implemented
+If you've forgotten your username or password, RT can <A
+href="/NoAuth/Reminder.html">send you a reminder</a>.
+-->
+<BR>
+<HR>
+RT is &copy; Copyright 1996-2002 Jesse Vincent &lt;jesse@bestpractical.com&gt;.  It is
+distributed under <a href="http://www.gnu.org/copyleft/gpl.html">Version 2 of the GNU General Public License.</a>
+
+
+<%ARGS>
+$user => ""
+$pass => undef
+$goto => undef
+$Error => undef
+</%ARGS>
+
+<%INIT>
+SetContentType('text/html');
+</%INIT>
diff --git a/rt/webrt/Elements/MessageBox b/rt/webrt/Elements/MessageBox
new file mode 100644 (file)
index 0000000..aa081a3
--- /dev/null
@@ -0,0 +1,30 @@
+<TEXTAREA COLS=<%$Width%> ROWS=15 WRAP=HARD NAME="<%$Name%>"><% $Default %><%$message%><%$signature%></TEXTAREA>
+<%INIT>
+
+my ($message);
+
+if ($MessageURI) {
+       my $code;
+       ($code, $Default)=RT::Link->GetContent($MessageURI);
+}
+if ($QuoteTransaction) {
+    my $transaction=RT::Transaction->new($session{'CurrentUser'});
+    $transaction->Load($QuoteTransaction);
+    $message=$transaction->Content(Quote => 1);
+}
+
+my $signature = '';
+if ($session{'CurrentUser'}->UserObj->Signature) {
+       $signature = "-- \n".$session{'CurrentUser'}->UserObj->Signature;
+}
+
+</%INIT>
+<%ARGS>
+$QuoteTransaction => undef
+$Name => 'Content'
+$Default => ''
+$DefaultURI => undef
+$Width => 72
+$MessageURI => undef
+</%ARGS>
+
diff --git a/rt/webrt/Elements/MyRequests b/rt/webrt/Elements/MyRequests
new file mode 100644 (file)
index 0000000..6781729
--- /dev/null
@@ -0,0 +1,45 @@
+<& /Elements/TitleBoxStart, title => "25 highest priority tickets I requested..." &>
+<TABLE BORDER=0 cellspacing=0 cellpadding=1 WIDTH=100%>
+<TR>
+<TH align=right>#</TH>
+<TH align=left>Subject</TH>
+<TH align=left>Queue</TH>
+<TH align=left>Status</TH>
+<TH align=left>Owner</TH>
+<TH>&nbsp;</TH>
+</TR>
+% while (my $Ticket = $MyTickets->Next) {
+<TR>
+<TD ALIGN=RIGHT>
+<%$Ticket->Id%>
+</TD>
+<TD>
+<A HREF="<% $RT::WebPath %>/Ticket/Display.html?id=<%$Ticket->Id%>">
+<%$Ticket->Subject || '[no subject]'%>
+</A>
+</TD>
+<TD>
+<%$Ticket->QueueObj->Name%>
+</TD><TD>
+<%$Ticket->Status%>
+</TD><TD>
+<%$Ticket->OwnerObj->Name%>
+</TD><TD ALIGN=RIGHT>
+[<A HREF="<% $RT::WebPath %>/Ticket/Display.html?id=<%$Ticket->Id%>">Display</A>]
+</TD>
+</TR>
+% }
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+
+
+<%INIT>
+my $MyTickets;
+$MyTickets = new RT::Tickets ($session{'CurrentUser'});
+$MyTickets->LimitRequestor(VALUE => $session{'CurrentUser'}->EmailAddress);
+$MyTickets->LimitStatus(VALUE => "open");
+$MyTickets->LimitStatus(VALUE => "new");
+$MyTickets->OrderBy(FIELD => 'Priority', ORDER => 'DESC');
+$MyTickets->RowsPerPage(25);
+
+</%INIT>
diff --git a/rt/webrt/Elements/MyTickets b/rt/webrt/Elements/MyTickets
new file mode 100644 (file)
index 0000000..64a2ba7
--- /dev/null
@@ -0,0 +1,43 @@
+<& /Elements/TitleBoxStart, title => "25 highest priority tickets I own..." &>
+<TABLE BORDER=0 cellspacing=0 cellpadding=1 WIDTH=100%>
+<TR>
+<TH ALIGN=RIGHT>#</TH>
+<TH ALIGN=LEFT>Subject</TH>
+<TH ALIGN=LEFT>Queue</TH>
+<TH ALIGN=LEFT>Status</TH>
+<TH ALIGN=LEFT>&nbsp;</TH>
+</TR>
+% while (my $Ticket = $MyTickets->Next) {
+<TR>
+<TD ALIGN=RIGHT>
+<%$Ticket->Id%>
+</TD>
+<TD>
+<A HREF="<% $RT::WebPath %>/Ticket/Display.html?id=<%$Ticket->Id%>">
+<%$Ticket->Subject || '[no subject]'%>
+</A>
+</TD>
+<TD>
+<%$Ticket->QueueObj->Name%>
+</TD><TD>
+<%$Ticket->Status%>
+</TD>
+<TD ALIGN=RIGHT>
+[<A HREF="<% $RT::WebPath %>/Ticket/Update.html?id=<%$Ticket->Id%>">Update</A>]
+</TD>
+</TR>
+% }
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+
+
+<%INIT>
+my $MyTickets;
+$MyTickets = new RT::Tickets ($session{'CurrentUser'});
+$MyTickets->LimitOwner(VALUE => $session{'CurrentUser'}->Id);
+$MyTickets->LimitStatus(VALUE => "open");
+$MyTickets->LimitStatus(VALUE => "new");
+$MyTickets->OrderBy(FIELD => 'Priority', ORDER => 'DESC');
+$MyTickets->RowsPerPage(25);
+
+</%INIT>
diff --git a/rt/webrt/Elements/Quicksearch b/rt/webrt/Elements/Quicksearch
new file mode 100644 (file)
index 0000000..d44c996
--- /dev/null
@@ -0,0 +1,41 @@
+<& /Elements/TitleBoxStart, title => "Find new/open tickets", titleright => "<A class='inverse' href=\"$RT::WebPath/Search/Listing.html?NewSearch=1\">Advanced Search</A>" &>
+
+<TABLE BORDER=0 cellspacing=0 cellpadding=1 WIDTH=100%>                       
+<tr>                                                                          
+       <th align=left>Queue</th>                                         
+       <th align=left><font size=-1>New</font></th>
+       <th align=left><font size=-1>Open</font></th>          
+       <th align=left><font size=-1>Stalled</font></th>          
+</tr>
+
+<%PERL>
+while (my $queue = $Queues->Next) {
+     $Tickets->ClearRestrictions;                                           
+     $Tickets->LimitStatus(VALUE => "open");                                
+     $Tickets->LimitQueue(VALUE => $queue->id, OPERATOR => '=');            
+     my $open = $Tickets->Count();
+
+     $Tickets->ClearRestrictions;                                           
+     $Tickets->LimitStatus(VALUE => "new");
+     $Tickets->LimitQueue(VALUE => $queue->id, OPERATOR => '=');            
+     my $new = $Tickets->Count();
+
+     $Tickets->ClearRestrictions; 
+     $Tickets->LimitStatus(VALUE => "stalled");
+     $Tickets->LimitQueue(VALUE => $queue->id, OPERATOR => '=');            
+     my $stalled = $Tickets->Count();
+</%PERL>
+<TR><TD><A HREF="<% $RT::WebPath%>/Search/Listing.html?ValueOfStatus=open&ValueOfStatus=new&StatusOp=%3D&QueueOp=%3D&ValueOfQueue=<%$queue->Id%>&RowsPerPage=50&NewSearch=1"><%$queue->Name%></a></TD>
+<TD><%$new%></TD>
+<TD><%$open%></TD>
+<TD><%$stalled%></TD>
+</TR>
+% }
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+
+<%INIT>
+my $Queues = new RT::Queues($session{'CurrentUser'}); 
+$Queues->UnLimit();
+my $Tickets = new RT::Tickets ($session{'CurrentUser'});
+</%INIT>
diff --git a/rt/webrt/Elements/Refresh b/rt/webrt/Elements/Refresh
new file mode 100644 (file)
index 0000000..6949d8c
--- /dev/null
@@ -0,0 +1,22 @@
+<SELECT NAME="<%$Name%>">
+<OPTION VALUE="-1"
+%unless ($Default) {
+ SELECTED
+%}
+>Don't refresh this page.</OPTION>
+%foreach my $value (@refreshevery) {
+<OPTION VALUE="<%$value%>"
+% if ($value == $Default) {
+SELECTED 
+% }
+>Refresh this page every <%$value/60%> minutes.
+%}
+</SELECT>
+
+<%INIT>
+my @refreshevery = qw(120 300 600 1200 3600 7200);
+</%INIT>
+<%ARGS>
+$Name => undef
+$Default => undef
+</%ARGS>
diff --git a/rt/webrt/Elements/Section b/rt/webrt/Elements/Section
new file mode 100755 (executable)
index 0000000..067311d
--- /dev/null
@@ -0,0 +1,11 @@
+<TABLE WIDTH=100%>
+<TR>
+<TD>
+<font size=+4><%$title%></font>
+</TD>
+</TR>
+</TABLE>
+
+<%ARGS>
+$title => undef
+</%ARGS>
\ No newline at end of file
diff --git a/rt/webrt/Elements/SelectBoolean b/rt/webrt/Elements/SelectBoolean
new file mode 100755 (executable)
index 0000000..93b78ce
--- /dev/null
@@ -0,0 +1,24 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/SelectBoolean,v 1.1 2002-08-12 06:17:08 ivan Exp $
+<SELECT NAME ="<%$Name%>">
+<OPTION VALUE="<%$TrueVal%>" <%$TrueDefault%>><%$True%></OPTION>
+<OPTION VALUE="<%$FalseVal%>" <%$FalseDefault%>><%$False%></OPTION>
+</SELECT>
+
+<%ARGS>
+$Name => undef
+$True => "is"
+$Default => 'true'
+$TrueVal => 1
+$FalseVal => 0
+$False => "isn't"
+</%ARGS>
+
+<%INIT>
+my ($TrueDefault, $FalseDefault);
+if ($Default && $Default !~ /true/i) {
+       $FalseDefault = "SELECTED";
+}
+else {
+       $TrueDefault = "SELECTED";
+}
+</%INIT>
diff --git a/rt/webrt/Elements/SelectDate b/rt/webrt/Elements/SelectDate
new file mode 100755 (executable)
index 0000000..6fafbf1
--- /dev/null
@@ -0,0 +1,25 @@
+<INPUT NAME="<%$Name%>" VALUE="<%$Default%>" size=16> 
+
+<%init>
+unless ((defined $Default) or 
+       ($current <= 0)) {
+       my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
+                                            localtime($current);
+        $Default = sprintf("%04d-%02d-%02d %02d:%02d",                         
+                           $year+1900,$mon+1,$mday,                            
+                           $hour,$min);   
+}
+
+unless ($Name) {
+       $Name = $menu_prefix. "_Date";
+}
+</%init>
+
+<%args>
+
+$ShowTime => undef
+$menu_prefix=>''
+$current=>time
+$Default => undef
+$Name => undef
+</%args>
diff --git a/rt/webrt/Elements/SelectDateRelation b/rt/webrt/Elements/SelectDateRelation
new file mode 100755 (executable)
index 0000000..c5849c8
--- /dev/null
@@ -0,0 +1,14 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/SelectDateRelation,v 1.1 2002-08-12 06:17:08 ivan Exp $
+<SELECT NAME ="<%$Name%>">
+<OPTION VALUE="&lt;"><%$Before%></OPTION>
+<OPTION VALUE="="><%$On%></OPTION>
+<OPTION VALUE="&gt;"><%$After%></OPTION>
+</SELECT>
+
+<%ARGS>
+$Name => undef
+$Default => undef
+$Before => 'Before'
+$On =>         'On'
+$After => 'After'
+</%ARGS>
diff --git a/rt/webrt/Elements/SelectDateType b/rt/webrt/Elements/SelectDateType
new file mode 100755 (executable)
index 0000000..65c0e9b
--- /dev/null
@@ -0,0 +1,12 @@
+<SELECT NAME="<%$Name%>">
+<OPTION VALUE="Created">Created</OPTION>
+<OPTION VALUE="Started">Started</OPTION>
+<OPTION VALUE="Resolved">Resolved</OPTION>
+<OPTION VALUE="Told">Last Contacted</OPTION>
+<OPTION VALUE="LastUpdated">Last Updated</OPTION>
+<OPTION VALUE="StartsBy">Starts By</OPTION>
+<OPTION VALUE="Due">Due</OPTION>
+</SELECT>
+<%ARGS>
+$Name => 'DateType'
+</%ARGS>
diff --git a/rt/webrt/Elements/SelectEqualityOperator b/rt/webrt/Elements/SelectEqualityOperator
new file mode 100755 (executable)
index 0000000..f93dc1a
--- /dev/null
@@ -0,0 +1,18 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/SelectEqualityOperator,v 1.1 2002-08-12 06:17:08 ivan Exp $
+<SELECT NAME ="<%$Name%>">
+% while (my $option = shift @Options) {
+% my $value = shift @Values;
+<OPTION VALUE="<%$value%>"
+% if ($Default eq '$value') {
+SELECTED
+% }
+><%$option%></OPTION>
+% }
+</SELECT>
+
+<%ARGS>
+$Name => undef
+@Options => ('less than', 'equal to', 'greater than', 'not equal to')
+@Values => qw(< = > !=)
+$Default => undef
+</%ARGS>
diff --git a/rt/webrt/Elements/SelectKeyword b/rt/webrt/Elements/SelectKeyword
new file mode 100644 (file)
index 0000000..c4bd9e1
--- /dev/null
@@ -0,0 +1,38 @@
+<SELECT NAME=<%$Name%> <%$Size%> <%$Multiple%>>
+<OPTION VALUE="">-</OPTION>
+<OPTION VALUE="NULL">(empty)</OPTION>
+%   foreach my $kid ( keys %{$Descendents} ) {
+<OPTION VALUE="<% $kid %>" 
+%if ($kid == $Default) {
+SELECTED
+%}
+><% $Descendents->{$kid} %></OPTION>
+% }
+</SELECT>
+
+
+<%INIT>
+
+unless (defined $KeywordObj) {
+    $KeywordObj = new RT::Keyword($session{'CurrentUser'});
+    $KeywordObj->Load($Root);
+}
+my $Descendents = $KeywordObj->Descendents();
+
+if ($Multiple) {
+       $Multiple = "MULTIPLE";
+}
+if ($Size) {
+       $Size="SIZE=$Size";
+}      
+
+
+</%INIT>
+<%ARGS>
+$Multiple => undef
+$Size => undef
+$Name => 'Keyword'
+$KeywordObj => undef
+$Root => 0
+$Default => undef
+</%ARGS>
diff --git a/rt/webrt/Elements/SelectKeywordOptions b/rt/webrt/Elements/SelectKeywordOptions
new file mode 100644 (file)
index 0000000..f56dfe5
--- /dev/null
@@ -0,0 +1,18 @@
+<PERL>
+while (my $keyword = $keywords->Next()) {
+    my ($selected);
+    if $keyword->Id == $default
+</PERL>
+<OPTION VALUE="<%$keyword->id%>"><% '-' x $depth %><%$keyword->Name%></OPTION>
+<& SelectKeywordOptions, depth => ($depth+1), root => $keyword->id &>
+%}
+<%INIT>
+
+my $keywords = new RT::Keywords($session{'CurrentUser'});
+$keywords->LimitToParent($root);
+
+</%INIT>
+<%ARGS>
+$root => undef
+$depth => 0
+</%ARGS>
\ No newline at end of file
diff --git a/rt/webrt/Elements/SelectLinkType b/rt/webrt/Elements/SelectLinkType
new file mode 100644 (file)
index 0000000..22cde3d
--- /dev/null
@@ -0,0 +1,16 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/SelectLinkType,v 1.1 2002-08-12 06:17:08 ivan Exp $
+
+<SELECT NAME ="<%$Name%>">
+% foreach ('MemberOf', 'DependsOn', 'RefersTo') { # TODO: Merging!
+<OPTION VALUE="<%$_%>"><%$_%></OPTION>
+% }
+</SELECT>
+
+<%ARGS>
+$Name => "LinkType"
+$Default => undef
+</%ARGS>
+
+<%INIT>
+# TODO handle Default
+</%INIT>
diff --git a/rt/webrt/Elements/SelectMatch b/rt/webrt/Elements/SelectMatch
new file mode 100644 (file)
index 0000000..7f3a94f
--- /dev/null
@@ -0,0 +1,31 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/SelectMatch,v 1.1 2002-08-12 06:17:08 ivan Exp $
+<SELECT NAME ="<%$Name%>">
+<OPTION VALUE="LIKE" <%$LikeDefault%>><%$Like%></OPTION>
+<OPTION VALUE="NOT LIKE" <%$NotLikeDefault%>><%$NotLike%></OPTION>
+<OPTION VALUE="=" <%$TrueDefault%>><%$True%></OPTION>
+<OPTION VALUE="!=" <%$FalseDefault%>><%$False%></OPTION>
+</SELECT>
+
+<%ARGS>
+$Name => undef
+$Like => 'contains'
+$NotLike => "doesn't contain"
+$True => 'is'
+$False => "isn't"
+$Default => undef
+</%ARGS>
+<%INIT>
+my ($TrueDefault, $FalseDefault, $LikeDefault, $NotLikeDefault);
+if ($Default && $Default !~ /true/i) {
+       $FalseDefault = "SELECTED";
+}
+elsif ($Default && $Default !~ /false/i) {
+       $TrueDefault = "SELECTED";
+} 
+elsif ($Default && $Default !~ /notlike/i) {
+       $NotLikeDefault = "SELECTED";
+}
+else {
+       $LikeDefault = "SELECTED";
+}
+</%INIT>
diff --git a/rt/webrt/Elements/SelectNewTicketQueue b/rt/webrt/Elements/SelectNewTicketQueue
new file mode 100755 (executable)
index 0000000..9f5cd28
--- /dev/null
@@ -0,0 +1,9 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/SelectNewTicketQueue,v 1.1 2002-08-12 06:17:08 ivan Exp $
+<& SelectQueue, Name => $Name, Verbose => $Verbose, Default => $Default,
+       ShowAllQueues => 0, ShowNullOption => 0 &>
+
+<%ARGS>
+$Name => 'Queue'
+$Verbose => undef
+$Default => undef
+</%ARGS>
diff --git a/rt/webrt/Elements/SelectOwner b/rt/webrt/Elements/SelectOwner
new file mode 100755 (executable)
index 0000000..59ebf36
--- /dev/null
@@ -0,0 +1,22 @@
+<SELECT NAME="<%$Name%>">
+<OPTION VALUE="">-</OPTION>
+<OPTION <% ($RT::Nobody->Id() == $Default) && "SELECTED" %> VALUE="<%$RT::Nobody->Id%>"><%$RT::Nobody->Name%></OPTION>
+%while ( my $User = $Users->Next())  {
+% if ((!defined $QueueObj) || ($User->HasQueueRight(Right => 'OwnTicket', QueueObj => $QueueObj, TicketObj => $TicketObj))){
+<OPTION VALUE="<%$User->Id()%>" <% ($User->Id() == $Default) && "SELECTED" %>><%$User->Name()%></OPTION>
+% }
+%}
+</SELECT>
+
+<%INIT>
+my $Users = RT::Users->new($session{CurrentUser});
+$Users->LimitToPrivileged;
+</%INIT>
+
+<%ARGS>
+$QueueObj => undef
+$Name => undef
+$Default => undef
+$User => undef
+$TicketObj => undef
+</%ARGS>
diff --git a/rt/webrt/Elements/SelectQueue b/rt/webrt/Elements/SelectQueue
new file mode 100755 (executable)
index 0000000..d63b17b
--- /dev/null
@@ -0,0 +1,38 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/SelectQueue,v 1.1 2002-08-12 06:17:08 ivan Exp $
+
+% if ($Lite) {
+<INPUT NAME="<%$Name%>" size=25 DEFAULT="<%$d->Name%>">
+% } else {
+<SELECT NAME ="<%$Name%>">
+% if ($ShowNullOption) {
+<OPTION VALUE="">-</OPTION>
+% }
+% while (my $queue=$q->Next) {
+% if ($ShowAllQueues || $queue->CurrentUserHasRight('CreateTicket')) {
+<OPTION VALUE="<%$queue->Id%>" <%($queue->Id == $Default) && 'SELECTED'%>><%$queue->Name%>
+%   if (($Verbose) and ($queue->Description) ){
+(<%$queue->Description%>)
+%  }
+</OPTION>
+% }
+% }
+</SELECT>
+% }
+<%ARGS>
+$ShowNullOption => 1
+$ShowAllQueues => 1
+$Name => undef
+$Verbose => undef
+$Default => undef
+$Lite => 0
+</%ARGS>
+
+<%INIT>
+
+my $q=new RT::Queues($session{'CurrentUser'});
+$q->UnLimit;
+
+my $d = new RT::Queue($session{'CurrentUser'});
+$d->Load($Default);
+
+</%INIT>
diff --git a/rt/webrt/Elements/SelectResultsPerPage b/rt/webrt/Elements/SelectResultsPerPage
new file mode 100644 (file)
index 0000000..0699c68
--- /dev/null
@@ -0,0 +1,22 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/SelectResultsPerPage,v 1.1 2002-08-12 06:17:08 ivan Exp $
+
+%# TODO: Better default handling
+
+<SELECT NAME ="<%$Name%>">
+% foreach my $value (@values) {
+<OPTION VALUE="<%$value%>" <% $value == $Default && 'SELECTED' %>>
+<% shift @labels %>
+</OPTION>
+% }
+</SELECT>
+
+<%INIT>
+my @values = qw(0 10 25 50 100);
+my @labels = qw(Unlimited 10 25 50 100);
+</%INIT>
+<%ARGS>
+
+$Name => undef
+$Default => 50
+
+</%ARGS>
diff --git a/rt/webrt/Elements/SelectSortOrder b/rt/webrt/Elements/SelectSortOrder
new file mode 100644 (file)
index 0000000..6dc9006
--- /dev/null
@@ -0,0 +1,18 @@
+<SELECT NAME="<%$Name%>">
+%foreach my $order (@orders) {
+<OPTION VALUE="<%$order%>" <%$order eq $Default && 'SELECTED' %>>
+<% shift @order_names %>
+</OPTION>
+% }
+</SELECT>
+
+<%INIT>
+my @orders = qw (ASC DESC);
+my @order_names = qw (Ascending Descending);
+
+</%INIT>
+
+<%ARGS>
+$Name => 'SortOrder'
+$Default => 'ASC'
+</%ARGS>
diff --git a/rt/webrt/Elements/SelectStatus b/rt/webrt/Elements/SelectStatus
new file mode 100755 (executable)
index 0000000..92df7c6
--- /dev/null
@@ -0,0 +1,17 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/SelectStatus,v 1.1 2002-08-12 06:17:08 ivan Exp $
+
+<SELECT NAME ="<%$Name%>">
+<OPTION VALUE="">-</OPTION>
+%foreach my $status (@status) {
+<OPTION VALUE="<%$status%>" <%($Default eq $status) && 'SELECTED'%>><%$status%></OPTION>
+% }
+</SELECT>
+<%ONCE>
+my $queue = new RT::Queue($session{'CurrentUser'});
+my @status = $queue->StatusArray();
+</%ONCE>
+<%ARGS>
+$Name => undef
+$Default => undef
+</%ARGS>
diff --git a/rt/webrt/Elements/SelectTicketSortBy b/rt/webrt/Elements/SelectTicketSortBy
new file mode 100644 (file)
index 0000000..02021de
--- /dev/null
@@ -0,0 +1,15 @@
+<SELECT NAME="<%$Name%>">
+% foreach my $field (@sortfields) {
+<OPTION VALUE="<%$field%>" <%$field eq $Default && 'SELECTED'%>><%$field%></OPTION>
+% }
+</SELECT>
+
+<%INIT>
+my $tickets = new RT::Tickets($session{'CurrentUser'});
+my @sortfields = $tickets->SortFields();
+
+</%INIT>
+<%ARGS>
+$Name => 'SortTicketsBy'
+$Default => 'id'
+</%ARGS>
diff --git a/rt/webrt/Elements/SelectUsers b/rt/webrt/Elements/SelectUsers
new file mode 100755 (executable)
index 0000000..f517d35
--- /dev/null
@@ -0,0 +1,8 @@
+<select name="UserField">
+<option value="Name">User Id
+<option value="EmailAddress">Email
+<option value="RealName">Name
+<option value="Organization">Organization
+</select>
+<& /Elements/SelectMatch, Name=> 'UserOp' &>
+<input size=8 name="UserString">
diff --git a/rt/webrt/Elements/SelectWatcherType b/rt/webrt/Elements/SelectWatcherType
new file mode 100644 (file)
index 0000000..5a85519
--- /dev/null
@@ -0,0 +1,26 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Elements/Attic/SelectWatcherType,v 1.1 2002-08-12 06:17:08 ivan Exp $
+%# portions Copyright 2000 Tobias Brox <tobix@fsck.com>
+%# Request Tracker is Copyright 1996-2000 Jesse Vincent <jesse@fsck.com>
+
+<SELECT NAME ="<%$Name%>">
+<OPTION VALUE="none">-</OPTION>
+%# Make nice options:
+%for my $option (@types) {
+<OPTION VALUE="<%$option%>" <%$option eq $Default && "SELECTED"%>><%$option%></OPTION>
+%}
+</SELECT>
+
+<%INIT>
+my @types;
+if ($Scope =~ 'queue') {
+   @types = qw(Cc AdminCc);
+}
+else { 
+   @types = qw(Requestor Cc AdminCc);
+}
+</%INIT>
+<%ARGS>
+$Default=>undef
+$Scope => 'ticket'
+$Name => 'WatcherType'
+</%ARGS>
diff --git a/rt/webrt/Elements/ShadedBox b/rt/webrt/Elements/ShadedBox
new file mode 100755 (executable)
index 0000000..334b579
--- /dev/null
@@ -0,0 +1,5 @@
+<div align="left"><span class=label><%$title |n %></span><br><b><%$content |n %></b></div>
+<%ARGS>
+$title => undef
+$content => "&nbsp;"
+</%ARGS>
diff --git a/rt/webrt/Elements/Submit b/rt/webrt/Elements/Submit
new file mode 100755 (executable)
index 0000000..7b75e9e
--- /dev/null
@@ -0,0 +1,44 @@
+<TABLE WIDTH=100% BGCOLOR="<%$color%>" CELLSPACING=0 BORDER=0 CELLPADDING=0 >
+<TR>
+% if ($Reset) {
+<TD>
+<FONT COLOR=#ffd800 >
+<INPUT TYPE=RESET VALUE="<%$ResetLabel%>">
+</FONT>
+</TD>
+%}
+<TD>
+&nbsp;
+</TD>
+<TD ALIGN=RIGHT VALIGN=CENTER>
+
+<FONT COLOR=#ffd800>
+
+% if ($AlternateLabel) {
+<B><%$AlternateCaption%>
+<INPUT TYPE=SUBMIT
+%if ($Name) {
+NAME="<%$Name%>"
+%}
+VALUE='<%$AlternateLabel%>'></B>
+% }
+
+<B><%$Caption%>
+<INPUT TYPE=SUBMIT
+%if ($Name) {
+NAME="<%$Name%>"
+% }
+ VALUE='<%$Label%>'></B></FONT>
+</TD>
+</TR>
+</TABLE>
+<%ARGS>
+$color => "#336699"
+$Caption => undef
+$AlternateCaption => undef
+$AlternateLabel => undef
+$Label => 'Submit'
+$Name => undef
+$Reset => undef
+$ResetLabel => 'Reset'
+</%ARGS>
diff --git a/rt/webrt/Elements/Tabs b/rt/webrt/Elements/Tabs
new file mode 100755 (executable)
index 0000000..6eacf39
--- /dev/null
@@ -0,0 +1,133 @@
+<TABLE WIDTH=100%>
+    <TR>
+      <TD VALIGN=TOP>
+       <TABLE cellspacing=1>
+         <TR>
+% foreach $tab (sort keys %{$toptabs}) {
+           <TD ALIGN=CENTER>
+             <font size=+1>
+               [<A 
+% if ($current_toptab eq $toptabs->{$tab}->{'path'}) {
+class='currenttab'
+% } 
+       HREF="<%$RT::WebPath%>/<% $toptabs->{$tab}->{'path'}%>"><% $toptabs->{$tab}->{'title'}%></A>]
+
+
+             </font>
+           </TD>
+% }
+         </TR>
+       </TABLE>
+<BR>
+% if ($tabs_scalar) {
+<% $tabs_scalar |n%>
+% }
+% if ($tabs) {
+       
+       <TABLE CELLSPACING=0 CELLPADDING=0 BORDER=0>    
+         <TR>
+% foreach $tab (sort keys %{$tabs}) {
+           <TD ALIGN=CENTER VALIGN=TOP>
+[<A 
+% if ($current_tab eq $tabs->{$tab}->{'path'}) {
+class='currenttab' 
+% } 
+HREF="<%$RT::WebPath%>/<% $tabs->{$tab}->{'path'}%>"><% $tabs->{$tab}->{'title'}%></A>]</TD>
+%}
+         </TR>
+       </TABLE>
+%}
+
+<BR>
+% if ($subtabs_scalar) {
+<% $subtabs_scalar |n%>
+% }
+% if ($subtabs) {
+       <TABLE> 
+         <TR>
+% foreach $tab (sort keys %{$subtabs}) {
+           <TD ALIGN=CENTER>
+             [<A HREF="<%$RT::WebPath%>/<% $subtabs->{$tab}->{'path'}%>"><% $subtabs->{$tab}->{'title'}%></A>]
+           </TD>
+%}
+         </TR>
+       </TABLE>
+%}
+      </TD>
+      <TD VALIGN=TOP ALIGN=RIGHT>
+<TABLE>
+<TR>
+
+% foreach $action (sort keys %{$topactions}) {
+<TD><font size=-1><%$topactions->{"$action"}->{'html'} |n %></font></TD>
+% }
+</TR>
+</TABLE>
+       
+
+       
+% if ($actions) {
+<TABLE><TR>
+%  foreach $action (sort keys %{$actions}) {
+<TD>
+<FONT SIZE=-1>
+% if ($actions->{"$action"}->{'html'}) {
+<%$actions->{"$action"}->{'html'} |n%>
+% } else {
+<A HREF="<%$RT::WebPath%>/<% $actions->{$action}->{'path'}%>"><% $actions->{$action}->{'title'}%></A>
+% }
+</FONT>
+</TD>
+%  }
+</TR></TABLE>
+% }
+       
+% if ($subactions_scalar) {
+<% $subactions_scalar |n%>
+% }
+% if ($subactions) {
+<BR>|&nbsp;
+%  foreach $action (sort keys %{$subactions}) {
+<%$subactions->{"$action"}->{'html'} |n%>&nbsp;|
+%  }
+% }
+      </TD>
+    </TR>
+  </TABLE>
+
+
+<%INIT>
+my ($tab, $action);
+my $toptabs = {    A => { title => 'Home',
+                           path => '',
+                         },
+                    B => { title => 'Search',
+                        path => 'Search/Listing.html'
+                      },
+
+                    D => { title => 'Configuration',
+                           path => 'Admin/'
+                         }
+                 };
+                    
+
+my $topactions = {
+       A => { html => $m->scomp('/Elements/CreateTicket')      
+               },
+       B => { html => $m->scomp('/Elements/GotoTicket') 
+               }
+       };
+</%INIT>
+<%ARGS>
+$current_toptab =>  "none"
+$current_tab => "none"
+$current_subtab => "none"
+$tabs => undef
+$tabs_scalar => undef
+$subtabs => undef
+$actions => undef
+$subactions => undef
+$subtabs_scalar => undef
+$subactions_scalar => undef
+</%ARGS>
+
diff --git a/rt/webrt/Elements/TitleBoxEnd b/rt/webrt/Elements/TitleBoxEnd
new file mode 100755 (executable)
index 0000000..bdd4106
--- /dev/null
@@ -0,0 +1,10 @@
+</TD></TR></TABLE>
+</TD>
+</TR>
+<TR><TD COLSPAN=4><IMG SRC="<%$RT::WebImagesURL%>spacer.gif" height=1 ALT="&nbsp;"width=1></TD>
+</TABLE>
+<%ARGS>
+$title => undef
+$content => undef
+</%ARGS>
+
diff --git a/rt/webrt/Elements/TitleBoxStart b/rt/webrt/Elements/TitleBoxStart
new file mode 100755 (executable)
index 0000000..6d0f1f9
--- /dev/null
@@ -0,0 +1,20 @@
+<TABLE CLASS="<%$class%>" BGCOLOR="<%$color%>" CELLSPACING=0 BORDER=0 CELLPADDING=0 WIDTH="<%$width%>">
+<TR><TD ROWSPAN=2><IMG SRC="<%$RT::WebImagesURL%>spacer.gif" width=1 height=1 ALT=""></TD>
+<TD valign=middle align=left bgcolor="<%$color%>">&nbsp;<font size=-1 color="#ffffff"><b><% $title_href && "<A CLASS=\"$title_class\" HREF=\"$title_href\">"|n%><%$title |n %><%  $title_href && "</A>" |n%></b></font></TD>
+<TD  ALIGN="right" valign=middle bgcolor="<%$color%>"><FONT color="#ffffff" SIZE=-1><%$titleright |n %></FONT>&nbsp;</TD>
+<TD ROWSPAN=2><IMG SRC="<%$RT::WebImagesURL%>spacer.gif" width=1 height=1 ALT=""></TD></TR>
+<TR><TD COLSPAN=2 bgcolor="<%$contentbg%>" valign=top align=left WIDTH=100%>
+<TABLE CELLPADDING=2 WIDTH=100%><TR><TD>
+<%ARGS>
+$width => "100%"
+$class => undef
+
+$title_href => undef
+$title => undef
+$title_class => undef
+
+$titleright_href => undef
+$titleright => undef
+$contentbg => "#dddddd"
+$color => "#336699"
+</%ARGS>
diff --git a/rt/webrt/Elements/ViewUser b/rt/webrt/Elements/ViewUser
new file mode 100644 (file)
index 0000000..92446e6
--- /dev/null
@@ -0,0 +1,29 @@
+
+<& /Elements/TitleBoxStart, 
+       title => "<a class='inverse' href=\"$RT::WebPath/Search/Listing.html?LimitRequestorById=1&IdOfRequestor=".$User->id."\">Tickets from $name</a>",
+       titleright=> "<a class='inverse' href=\"$RT::WebPath/EditUserComments.html?id=".$User->id."\">Comments about $name</a>" &>
+<TABLE WIDTH="100%">
+<tr>
+<td halign=left valign=top>
+%while (my $w=$tickets->Next) {
+<%$w->Id%>: <a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$w->id%>"><%$w->Subject%></a> (<%$w->Status%>)<BR>
+%}
+</td>
+<td align=right valign=top>
+       <% ($User->Comments || "No comment entered about this user") %>
+</tr>
+</table>
+<& /Elements/TitleBoxEnd &>
+
+<%ARGS>
+$User=>undef
+</%ARGS>
+
+<%INIT>
+my $name=$User->RealName || $User->EmailAddress;       
+
+my $tickets = new RT::Tickets($session{'CurrentUser'});
+$tickets->LimitRequestor(VALUE => $User->EmailAddress);
+
+
+</%INIT>
diff --git a/rt/webrt/Elements/dayMenu b/rt/webrt/Elements/dayMenu
new file mode 100755 (executable)
index 0000000..6591b05
--- /dev/null
@@ -0,0 +1,19 @@
+<%doc>-------------------------------------------------------------------
+dayMenu: Display a pulldown menu of days of the month (1 to 31)
+
+Optional arguments:
+$menu_name - Name of menu, defaults to 'day'
+$current - Selected day value (1 to 31)
+-------------------------------------------------------------------</%doc>
+
+<select name="<% $menu_name %>">
+<option value="-1">-
+% foreach my $day (1..31) {
+<option value="<% $day %>" <% $day==$current ? "selected" : "" %>><% sprintf("%02d",$day) %>
+% }
+</select>
+
+<%args>
+$menu_name=>'day'
+$current=>undef
+</%args>
diff --git a/rt/webrt/Elements/monthMenu b/rt/webrt/Elements/monthMenu
new file mode 100755 (executable)
index 0000000..b9a71d3
--- /dev/null
@@ -0,0 +1,37 @@
+<%doc>-------------------------------------------------------------------
+monthMenu: Display a pulldown menu of months
+
+Optional arguments:
+$menu_name - Name of menu, defaults to 'month'
+$current - Selected month value (1 to 12)
+$format - Choice of month labels:
+   'full'    (January, February, ...)
+   'short'   (Jan, Feb, ...)
+   'numeric' (1, 2, ...)
+  Defaults to 'full'. The format only affects appearance; the menu
+  values are always numeric.
+-------------------------------------------------------------------</%doc>
+
+<select name="<% $menu_name %>">
+<option value="-1">-
+% foreach my $month (1..12) {
+<option value="<% $month %>" <% $month==$current ? "selected" : "" %>>
+%   if ($format eq 'full') {
+<% $month_names[$month-1] %>
+%   } elsif ($format eq 'short') {
+<% substr($month_names[$month-1],0,3) %>
+%   } elsif ($format eq 'numeric') {
+<% sprintf("%02d",$month) %>
+%   }
+% }
+</select>
+
+<%init>
+my @month_names = qw(January February March April May June July August September October November December);
+</%init>
+
+<%args>
+$menu_name=>'month'
+$current=>undef
+$format=>'full'
+</%args>
diff --git a/rt/webrt/Elements/yearMenu b/rt/webrt/Elements/yearMenu
new file mode 100755 (executable)
index 0000000..4a0e7a7
--- /dev/null
@@ -0,0 +1,24 @@
+<%doc>-------------------------------------------------------------------
+yearMenu: Display a pulldown menu of years.
+
+Optional arguments:
+$menu_name - Name of menu, defaults to 'year'
+$current - Selected year value
+$min - Minimum year appearing in menu; defaults to current year
+$max - Maximum year appearing in menus; defaults to $min plus 10.
+-------------------------------------------------------------------</%doc>
+
+<select name="<% $menu_name %>">
+<option value="-1">-
+% foreach my $year ($min..$max) {
+<option value="<% $year %>" <% $year==$current ? "selected" : "" %>>
+<% $year %>
+% }
+</select>
+
+<%args>
+$menu_name=>'year'
+$current=>(localtime)[5]+1900
+$min=>(localtime)[5]+1900-1
+$max=>$min+10
+</%args>
diff --git a/rt/webrt/NoAuth/Logout.html b/rt/webrt/NoAuth/Logout.html
new file mode 100755 (executable)
index 0000000..a00ae96
--- /dev/null
@@ -0,0 +1,22 @@
+<HTML>
+<HEAD>
+<TITLE>RT: Logout</TITLE>
+ <META HTTP-EQUIV="Refresh" CONTENT="0;URL=<%$RT::WebPath%>/">
+</HEAD>
+<BODY>
+<p>You have been logged out of RT.
+
+
+<br>
+<br>
+<A HREF="<%$RT::WebPath%>/">You're welcome to login again</a>
+
+
+<%PERL>
+if (defined %session) {
+       %session = undef;
+}
+$m->abort();
+</%PERL>
+
+
diff --git a/rt/webrt/NoAuth/Reminder.html b/rt/webrt/NoAuth/Reminder.html
new file mode 100755 (executable)
index 0000000..a814a91
--- /dev/null
@@ -0,0 +1,3 @@
+<& /Elements/Header, title => 'Password Reminder' &>
+
+Not yet implemented.
diff --git a/rt/webrt/NoAuth/images/rt.jpg b/rt/webrt/NoAuth/images/rt.jpg
new file mode 100644 (file)
index 0000000..a137a93
Binary files /dev/null and b/rt/webrt/NoAuth/images/rt.jpg differ
diff --git a/rt/webrt/NoAuth/images/spacer.gif b/rt/webrt/NoAuth/images/spacer.gif
new file mode 100644 (file)
index 0000000..5bfd67a
Binary files /dev/null and b/rt/webrt/NoAuth/images/spacer.gif differ
diff --git a/rt/webrt/NoAuth/webrt.css b/rt/webrt/NoAuth/webrt.css
new file mode 100755 (executable)
index 0000000..a71d057
--- /dev/null
@@ -0,0 +1,102 @@
+BODY, TD {font-family: Helvetica, Arial, sans-serif}
+TD {border-color: #cccccc }
+
+BLOCKQUOTE.message {
+       font-size: 80%;
+       font-family: "Helvetica", sans-serif;
+}
+
+
+BODY {
+  color: #000;
+  background: #FFFFFF;
+  font-family: "Helvetica", sans-serif;
+
+
+}
+
+TD, TH { /* ns workaround */
+  font-family: "Helvetica", sans-serif;
+}
+
+TR.oddline { 
+    background-color : #eeeeee;
+}
+
+H1, H2, H3 { 
+  margin-top: 0.2em;
+  color: #336699;
+  font-family: "Helvetica", sans-serif;
+
+  clear: both;
+}
+
+
+DIV.endmatter { margin-left: -7% }
+
+
+
+A { font-weight: bold; 
+               color: #000000;
+           /* border: none -- breaks NS 4.x */ }
+
+.currenttab { background-color: #cccccc; }
+
+.inverse { color: #ffffff; }
+
+
+
+A:link IMG, A:visited IMG { border-style: none }
+
+A IMG { color: white } /* The only way to hide the border in NS 4.x */
+
+.hide {
+  display: none;
+  color: white;
+}
+
+SPAN.date { font-size: 0.8em }
+
+SPAN.attribution {
+  font-weight: bold
+}
+
+SPAN.label { font-size: 0.8em; 
+}
+
+BLOCKQUOTE {
+  font-style: italic;
+  /* color: #990; */
+}
+
+ADDRESS { 
+  text-align: right;
+  font-weight: bold;
+  font-style: italic 
+}
+
+BLOCKQUOTE P {                 /* Try to avoid space above the attribution */
+  margin-bottom: 0;
+}
+BLOCKQUOTE ADDRESS {
+  margin: 0;
+}
+
+.motto, .motto A {font: italic 120%/1.3 Georgia, serif; color: #990}
+
+.emphasized {
+  font-weight: bold
+}
+
+/* Why o why does this break Netscape 4.x?
+IMG { 
+  border: none
+}
+*/
+
+P.map-also { font-style: italic; margin-left: 15%; text-align: right }
+
+.oddline { 
+background-color : #eeeeee;
+
+}
diff --git a/rt/webrt/Search/Bulk.html b/rt/webrt/Search/Bulk.html
new file mode 100755 (executable)
index 0000000..ac688d7
--- /dev/null
@@ -0,0 +1,186 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Search/Attic/Bulk.html,v 1.1 2002-08-12 06:17:09 ivan Exp $
+%# Copyright 1996-2001 Jesse Vincent <jesse@fsck.com>
+<& /Elements/Header, Title => "Bulk ticket update" &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<FORM METHOD=POST>
+<TABLE WIDTH=100% border=0 cellpadding=3 CELLSPACING=0>
+<TR>
+<TH>Update</TH>
+%foreach my $col (@cols) {
+% my $colalias = $col;
+% $colalias =~ s/(Obj\-\>|)(Name|AsString)//;
+
+<TH ><% $colalias %>&nbsp;</TH>
+%}
+</TR>
+
+<%PERL>
+
+my $i;
+
+
+      
+$session{'tickets'}->RedoSearch();
+while (my $Ticket = $session{'tickets'}->Next) {
+ $i++;
+ if ($i % 2) {
+     $bgcolor = "#dddddd";
+ }
+ else {
+     $bgcolor = "#ffffff";
+ }
+      </%PERL>
+<TR bgcolor="<%$bgcolor%>">
+<TD><input type=checkbox name="UpdateTicket<%$Ticket->Id%>" CHECKED></TD>
+%# The ticket view is controlled by config.pm, WebOptions
+%foreach my $col (@cols) {
+<TD>
+% if ($col eq 'id') {
+<A HREF="<% $RT::WebPath%>/Ticket/Display.html?id=<%$Ticket->Id%>"><%$Ticket->Id()%></A>
+% }
+%else {
+<% eval "\$Ticket->$col()" %>&nbsp;
+%}
+</TD>
+%}
+</TR>
+%}
+
+
+
+</TABLE>
+
+<HR>
+
+
+<& /Elements/TitleBoxStart, title => 'Update selected tickets' &>
+<TABLE>
+<TR>
+<TD VALIGN=TOP>
+<UL>
+<li> Make Owner <& /Elements/SelectOwner, Name => "Owner" &>
+(<input type=checkbox name="ForceOwnerChange"> Force change)
+<li> Add Requestor <INPUT Name="AddRequestor" SIZE=20>
+<li> Remove Requestor <INPUT Name="DeleteRequestor" SIZE=20>
+<li> Add Cc <INPUT Name="AddCc" SIZE=20>
+<li> Remove Cc <INPUT Name="DeleteCc" SIZE=20>
+<li> Add AdminCc <INPUT Name="AddAdminCc" SIZE=20>
+<li> Remove AdminCc <INPUT Name="DeleteAdminCc" SIZE=20>
+</UL>
+</TD>
+<TD VALIGN=TOP>
+<UL>
+<li> Make subject <INPUT Name="Subject" SIZE=20>
+<li> Make priority <INPUT Name="Priority" SIZE=4>
+<li> Make queue <& /Elements/SelectQueue, Name => "Queue" &>
+
+<li>Make Status <& /Elements/SelectStatus, Name => "Status" &>
+
+
+
+<li> Make date Starts <& /Elements/SelectDate, Name => "Starts_Date", ShowTime => 0, Default => '' &>
+<li> Make date Started <& /Elements/SelectDate, Name => "Started_Date", ShowTime => 0, Default => '' &>
+<li> Make date Told <& /Elements/SelectDate, Name => "Told_Date", ShowTime => 0, Default => '' &>
+<li> Make date Due <& /Elements/SelectDate, Name => "Due_Date", ShowTime => 0, Default => '' &>
+<li> Make date Resolved <& /Elements/SelectDate, Name => "Resolved_Date", ShowTime => 0, Default => '' &>
+
+
+% while ( my $KeywordSelect = $KeywordSelects->Next ) {
+
+<li> Add <% $KeywordSelect->Name %> <& /Elements/SelectKeyword, Name => "AddToKeywordSelect".$KeywordSelect->id, KeywordObj => $KeywordSelect->KeywordObj &>
+<li> Remove <% $KeywordSelect->Name %> <& /Elements/SelectKeyword, Name => "DeleteFromKeywordSelect".$KeywordSelect->id, KeywordObj => $KeywordSelect->KeywordObj &>
+% }
+
+</UL>
+
+
+</TD>
+</TR>
+</table>
+<& /Elements/TitleBoxEnd&>
+<& /Elements/TitleBoxStart, title => 'Add comments or replies to selected tickets' &>
+<table>
+<tr><td align=right>Update Type:</td>
+<td><select name="UpdateType">
+  <option value="private" >Comments (not sent to requestors)</option>
+<option value="response" >Response to requestors</option>
+</select> 
+</td></tr>
+<tr><td align=right>Subject:</td><td> <input name="UpdateSubject" size=60 value=""></td></tr>
+ <tr><td align=right>Attach:</td><td><input name="UpdateAttachment" type="file"></td></tr>
+ <tr><td colspan="2">
+ <& /Elements/MessageBox, Name=>"UpdateContent"&>
+ </td></tr>
+ </table>
+<& /Elements/TitleBoxEnd &>
+
+
+
+
+<& /Elements/Submit &>
+
+
+</FORM>
+<%INIT>
+
+# Iterate through the ARGS hash and remove anything with a null value.
+map ($ARGS{$_} =~ /^$/ && (delete $ARGS{$_}), keys %ARGS);
+
+my ($bgcolor, @results);
+my @cols = qw(id Status Priority Subject QueueObj->Name OwnerObj->Name RequestorsAsString DueAsString );
+
+Abort("No search to operate on.") unless ($session{'tickets'});
+
+
+my $do_comment_reply=0;
+# Prepare for ticket updates
+$ARGS{'UpdateContent'} =~ s/\r\n/\n/g;
+chomp ($ARGS{'UpdateContent'}) ;
+
+if ($ARGS{'UpdateContent'} &&
+    $ARGS{'UpdateContent'} ne '' &&
+    $ARGS{'UpdateContent'} ne  "-- \n" .
+    $session{'CurrentUser'}->UserObj->Signature) {
+            $do_comment_reply=1;
+}
+
+my $KeywordSelects = new RT::KeywordSelects $session{'CurrentUser'};
+foreach ( $session{'tickets'}->RestrictionValues('Queue') ) {
+    $KeywordSelects->LimitToQueue($_);
+}
+
+$KeywordSelects->IncludeGlobals;
+
+
+#Iterate through each ticket we've been handed
+
+while (my $Ticket = $session{'tickets'}->Next) {
+    $RT::Logger->debug( "Checking Ticket ".$Ticket->Id ."\n");
+    next unless ($ARGS{"UpdateTicket".$Ticket->Id});
+    $RT::Logger->debug ("Matched\n");
+    #Update the basics.
+    my @basicresults = ProcessTicketBasics(TicketObj => $Ticket, ARGSRef => \%ARGS);
+    my @dateresults = ProcessTicketDates(TicketObj => $Ticket, ARGSRef => \%ARGS);
+    my @watchresults = ProcessTicketWatchers(TicketObj => $Ticket, ARGSRef => \%ARGS);    
+    my @selectresults = ProcessTicketObjectKeywords(TicketObj => $Ticket, ARGSRef => \%ARGS);
+    
+   
+     my @updateresults; 
+     if ($do_comment_reply) {
+     ProcessUpdateMessage(TicketObj => $Ticket, ARGSRef => \%ARGS, Actions => \
+@updateresults); 
+    } 
+    my @tempresults = (@watchresults, @basicresults, @dateresults, @updateresults);
+    @tempresults = map { "Ticket ".$Ticket->Id. ": ".$_ } @tempresults;
+
+    
+    #Update the keyword selects
+    #Update the watchers
+    $RT::Logger->debug(join("\n",@tempresults));
+    @results = (@results, @tempresults);
+}
+
+</%INIT>
diff --git a/rt/webrt/Search/Listing.html b/rt/webrt/Search/Listing.html
new file mode 100755 (executable)
index 0000000..da927fe
--- /dev/null
@@ -0,0 +1,134 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Search/Attic/Listing.html,v 1.1 2002-08-12 06:17:09 ivan Exp $
+%# Copyright 1996-2001 Jesse Vincent <jesse@fsck.com>
+<& /Elements/Header, Title => "Search", Refresh => $session{'tickets_refresh_interval'} &>
+<& /Elements/Tabs, current_toptab => 'Search/Listing.html' &>
+
+
+% unless ($ARGS{'Action'} eq 'Refine') {
+<TABLE WIDTH=100% border=0 cellpadding=3 CELLSPACING=1>
+<TR>
+%foreach my $col (@{Config(\%ARGS, 'QueueListingCols')}) {
+<TH>
+
+<%PERL>        
+my ($order);
+ my $attr = $col->{'TicketAttribute'};
+ $attr =~ s/Obj->(Name|AsString|AgeAsString)//g;
+  if ($session{'tickets_sort_order'} =~ /^asc$/i) {
+   $order = 'DESC';
+ } else {
+   $order = 'ASC';
+ }
+</%PERL>
+
+% if (grep (/^$attr$/i, $session{'tickets'}->SortFields)) {
+<A 
+% if ($attr eq $session{'tickets_sort_by'}) {
+class="currenttab"
+% }
+HREF="<% $RT::WebPath%>/Search/Listing.html?Bookmark=<%$session{'tickets'}->FreezeLimits()|u%>&TicketsSortBy=<%$attr%>&TicketsSortOrder=<%$order%>&RowsPerPage=<%$session{'tickets_rows_per_page'}%>">
+<%$col->{Header}%>
+</A>
+% } else {
+<% $col->{Header} %>
+% }
+</TH>
+%}
+</TR>
+
+<%PERL>
+
+my $i;
+      
+$session{'tickets'}->RedoSearch();
+while (my $Ticket = $session{'tickets'}->Next) {
+ $i++;
+ if ($i % 2) {
+     $bgcolor = "#dddddd";
+ }
+ else {
+     $bgcolor = "#ffffff";
+ }
+      </%PERL>
+<TR bgcolor="<%$bgcolor%>" >
+%# The ticket view is controlled by config.pm, WebOptions
+%foreach my $col (@{Config(\%ARGS,'QueueListingCols')}) {
+<TD><& TicketCell , Ticket=>$Ticket,  Column=>$col &></TD>
+%}
+</TR>
+%}
+
+
+
+</TABLE>
+
+<div align=center>
+<font size=2>
+<a href="Listing.html?GotoPage=1">First page</a>
+&nbsp;&nbsp;
+<a href="Listing.html?GotoPage=Prev">&lt;Previous page</a>
+&nbsp;&nbsp;
+<a href="Listing.html?GotoPage=Next">Next page&gt;</a>
+%#&nbsp;&nbsp;<form method=get action="Listing.html">Goto page <input name=GotoPage size=2></form>
+</font>
+</div>
+% if ($session{'tickets'}->Count()) { 
+<div align=right>
+<a href="Bulk.html">Update all these tickets at once</a>
+</div>
+% }
+<HR>
+
+% } #endif {$ARGS{'Action'} eq 'Refine')
+<TABLE WIDTH="100%">
+<TR>
+<TD VALIGN="TOP">
+<& /Elements/TitleBoxStart, title => 'Search Criteria'&>
+
+<A HREF="<% $RT::WebPath%>/Search/Listing.html?ClearRestrictions=1">New search</a><br>
+<A HREF="<% $RT::WebPath%>/Search/Listing.html?Bookmark=<%$session{'tickets'}->FreezeLimits()|u%>&TicketsSortBy=<%$session{'tickets_sort_by'}%>&TicketsSortOrder=<%$session{'tickets_sort_order'}%>&RowsPerPage=<%$session{'tickets_rows_per_page'}%>">Bookmarkable URL for this search</a>
+<BR>
+<BR>
+% my %restrictions=$session{'tickets'}->DescribeRestrictions();
+% my %seen_restrictions=();
+% foreach $row (keys %restrictions){
+%  my $tmp=$restrictions{"$row"};
+%  if( ! defined( $seen_restrictions{"$tmp"} ) ){
+<%$restrictions{"$row"}%> <A HREF="<% $RT::WebPath%>/Search/Listing.html?DeleteRestriction=<%$row%>">[delete]</a><br>
+%   } else {
+%     $session{'tickets'}->DeleteRestriction($row);
+<b>Deleted Duplicate Restriction <i><%$tmp%></i></b><br>
+% }
+% $seen_restrictions{"$tmp"}++;
+%}
+<& /Elements/TitleBoxEnd&>
+</TD>
+<TD>
+
+<& PickRestriction &>
+
+</TD>
+</TR>
+</TABLE>
+
+<%INIT>
+
+my $bgcolor;
+require RT::Interface::Web;
+
+$session{'i'}++;
+if ($session{'tickets'}) {
+    if ( ($ARGS{'ClearRestrictions'}) ||
+        ($ARGS{'NewSearch'}) ) {
+       $session{'tickets'}->ClearRestrictions;
+    }
+    
+    if ($ARGS{'DeleteRestriction'}) {
+       $session{'tickets'}->DeleteRestriction($ARGS{'DeleteRestriction'});
+    }
+}
+&ProcessSearchQuery(ARGS=>\%ARGS);
+
+my $row;
+
+</%INIT>
diff --git a/rt/webrt/Search/PickRestriction b/rt/webrt/Search/PickRestriction
new file mode 100755 (executable)
index 0000000..82f576c
--- /dev/null
@@ -0,0 +1,112 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Search/Attic/PickRestriction,v 1.1 2002-08-12 06:17:09 ivan Exp $
+<FORM ACTION="Listing.html" METHOD="GET">
+<INPUT TYPE=HIDDEN NAME="Bookmark" VALUE="<% $session{'tickets'}->FreezeLimits()|u %>">
+<& /Elements/TitleBoxStart, title => 'Refine Search'&>
+<INPUT TYPE=HIDDEN NAME="CompileRestriction" VALUE=1>
+
+<ul>
+<li>Owner is  <& /Elements/SelectBoolean, Name => "OwnerOp", 
+                                         TrueVal=> '=', 
+                                         FalseVal => '!=' 
+&> 
+<& /Elements/SelectOwner, Name => "ValueOfOwner" &>
+
+<li>
+Requestor email address 
+<& /Elements/SelectMatch, Name => "RequestorOp" &>
+<INPUT Name="ValueOfRequestor" SIZE=20>
+
+<li>
+Subject <& /Elements/SelectMatch, Name => "SubjectOp" &> 
+<INPUT Name="ValueOfSubject" SIZE=20>
+
+<li>Queue  <& /Elements/SelectBoolean,  Name => "QueueOp" , 
+                                       True => "is", 
+                                       False => "isn't", 
+                                       TrueVal=> '=', 
+                                       FalseVal => '!=' &>
+<& /Elements/SelectQueue, Name => "ValueOfQueue" &>
+
+
+<li>Priority  <& /Elements/SelectEqualityOperator,  Name => "PriorityOp" &>
+
+<INPUT Name="ValueOfPriority" SIZE=5>
+
+
+<li>
+<& /Elements/SelectDateType, Name => 'DateType' &>
+<& /Elements/SelectDateRelation, Name=>"DateOp" &>
+<& /Elements/SelectDate, Name => "ValueOfDate", ShowTime => 0, Default => '' &>
+
+<li>Ticket content 
+<& /Elements/SelectBoolean, Name => "ContentOp", 
+                           True => "matches", 
+                           False => "does not match", 
+                           TrueVal => 'LIKE', 
+                           FalseVal => 'NOT LIKE' 
+&> 
+<Input Name="ValueOfContent" Size=20>
+
+<li>Status 
+<& /Elements/SelectBoolean, Name => "StatusOp", 
+                           True => "is", 
+                           False => "isn't", 
+                           TrueVal=> '=', 
+                           FalseVal => '!=' 
+&>  
+<& /Elements/SelectStatus, Name => "ValueOfStatus" &>
+
+% while ( my $KeywordSelect = $KeywordSelects->Next ) {
+
+<li><% $KeywordSelect->Name %> 
+       <& /Elements/SelectBoolean, Name => "KeywordSelectOp". $KeywordSelect->id, 
+                                   True => "is", False => "isn't", 
+                                   TrueVal=> '=', FalseVal => '!=' &>
+
+<& /Elements/SelectKeyword, Name => "KeywordSelect".$KeywordSelect->id,
+                           KeywordObj => $KeywordSelect->KeywordObj
+                           &>
+% }
+
+</UL>
+
+<& /Elements/TitleBoxEnd &>
+
+<& /Elements/TitleBoxStart, title => 'Ordering and sorting'&>
+
+<UL>
+
+<li>Results per page <& /Elements/SelectResultsPerPage, Name => "RowsPerPage", 
+                                                       Default => $session{'tickets_rows_per_page'} || '50'
+&>
+
+<li>Sort results by <& /Elements/SelectTicketSortBy, Name => "TicketsSortBy", 
+                                                    Default => $session{'tickets_sort_by'} 
+&> 
+<& /Elements/SelectSortOrder, Name => 'TicketsSortOrder', Default => $session{'tickets_sort_order'} &>
+
+<li> <& /Elements/Refresh, Name => 'RefreshSearchInterval' , Default => $session{'tickets_refresh_interval'} &>
+
+
+</UL>
+
+
+</DIV>
+
+
+
+<& /Elements/TitleBoxEnd &>
+
+<& /Elements/Submit, Label => 'Show Results', AlternateLabel => 'Refine', Name => 'Action'&>
+
+</FORM>
+
+
+ <%INIT>
+ my $KeywordSelects = new RT::KeywordSelects $session{'CurrentUser'};
+ foreach ( $session{'tickets'}->RestrictionValues('Queue') ) {
+        $KeywordSelects->LimitToQueue($_);
+ }
+
+ $KeywordSelects->IncludeGlobals;
+</%INIT>
diff --git a/rt/webrt/Search/RestrictSearch.html b/rt/webrt/Search/RestrictSearch.html
new file mode 100755 (executable)
index 0000000..977308e
--- /dev/null
@@ -0,0 +1,3 @@
+<& /Elements/Header, Title=>"Compile Restrictions" &>
+<& /Elements/Tabs &>
+<& PickRestriction &>
diff --git a/rt/webrt/Search/TicketCell b/rt/webrt/Search/TicketCell
new file mode 100644 (file)
index 0000000..aaded88
--- /dev/null
@@ -0,0 +1,28 @@
+%#$Header: /home/cvs/cvsroot/freeside/rt/webrt/Search/Attic/TicketCell,v 1.1 2002-08-12 06:17:09 ivan Exp $
+<% $link |n%><%$Column->{Constant} || eval("\$Ticket->$Column->{TicketAttribute}") || "-" %><% $endlink|n %>
+<%INIT>
+
+my $link = "";
+my $endlink = "";
+if ($Column->{TicketLink}) {
+       $link = "<A HREF=\"";
+       if ($Column->{TicketLink} == 1 ) {
+               $link .= "../Ticket/Display.html?";
+       }
+       else {
+               $link .= $Column->{TicketLink};
+       }
+
+       $link .= "id=".$Ticket->Id . $Column->{ExtraLinks};
+
+       if ($session{NewWindowOption}) {
+               $link .= "TARGET=\"TicketDisplay".$session{AlwaysNewWindowOption} && (time() . rand(1024))."\" ";
+       }
+       $link .= "\">";
+       $endlink = "</a>";
+}
+</%INIT>
+<%ARGS>
+$Ticket => undef
+$Column => undef
+</%ARGS>
diff --git a/rt/webrt/SelfService/Attachment/dhandler b/rt/webrt/SelfService/Attachment/dhandler
new file mode 100644 (file)
index 0000000..0d646cc
--- /dev/null
@@ -0,0 +1,27 @@
+<%perl>
+     my ($ticket, $trans,$attach, $filename);
+     my $arg = $m->dhandler_arg;                # get rest of path
+     if ($arg =~ '^(\d+)/(\d+)') {
+        $trans = $1;
+        $attach = $2;
+     }
+    else {
+        Abort("Corrupted attachment URL.");
+        }
+     my $AttachmentObj = new RT::Attachment($session{'CurrentUser'});
+     $AttachmentObj->Load($attach) || Abort("Attachment '$attach' could not be loaded");
+
+
+     unless ($AttachmentObj->id) {
+        Abort("Bad attachment id. Couldn't find attachment '$attach'\n");
+    }
+     unless ($AttachmentObj->TransactionId() == $trans ) {
+        Abort("Bad transaction number for attachment. $trans should be".$AttachmentObj->TransactionId() ."\n");
+
+     }
+     my $content_type = $AttachmentObj->ContentType || 'text/plain';
+     SetContentType($content_type);
+     $m->out($AttachmentObj->Content); 
+     $m->abort; 
+</%perl>
+
diff --git a/rt/webrt/SelfService/Closed.html b/rt/webrt/SelfService/Closed.html
new file mode 100644 (file)
index 0000000..a359360
--- /dev/null
@@ -0,0 +1,4 @@
+<& /SelfService/Elements/Header, title => 'RT Self Service / Closed Tickets' &>
+
+<& /SelfService/Elements/MyRequests, status => ['resolved'], friendly_status =>
+'closed' &>
diff --git a/rt/webrt/SelfService/Create.html b/rt/webrt/SelfService/Create.html
new file mode 100755 (executable)
index 0000000..60110cb
--- /dev/null
@@ -0,0 +1,63 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/SelfService/Attic/Create.html,v 1.1 2002-08-12 06:17:09 ivan Exp $
+%# Copyright 1996-2001 Jesse Vincent <jesse@fsck.com>
+
+<& Elements/Header, Title => "Create a request" &>
+
+
+<FORM ACTION="Display.html" METHOD="POST" ENCTYPE="multipart/form-data">
+<INPUT TYPE=HIDDEN Name="id" VALUE="new">
+<& /Elements/TitleBoxStart, contentbg => "#cccccc", title => "Create a new ticket" &>
+
+<TABLE>
+<TR>
+<TD>
+Queue:
+</TD>
+<TD>
+<& /Elements/SelectNewTicketQueue, Verbose => 'True' &>
+</TD>
+</TR>
+<TR>
+<TD>
+Requestors: 
+</TD>
+<TD>
+<INPUT Name="Requestors" Value="<%$session{CurrentUser}->EmailAddress%>" SIZE=20>
+</TD>
+</TR>
+<TR>
+<TD>
+Cc:
+</TD>
+<TD>
+ <INPUT NAME="Cc" SIZE=20>
+</TD>
+</TR>
+<TR>
+<TD>
+Subject:
+</TD>
+<TD>
+<INPUT Name="Subject" SIZE=60 MAXSIZE=100 value="">
+</TD>
+</TR>
+<TR>
+<TD>
+Attach file:
+</TD>
+<TD>
+<INPUT Name="Attach" type=file>
+</TD>
+</TR>
+<TR>
+<TD COLSPAN=2>
+Describe the issue below:<br>
+<& /Elements/MessageBox &>
+</TD>
+</TR>
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit, Label => "Create ticket"&>
+
+
+</FORM>
diff --git a/rt/webrt/SelfService/Display.html b/rt/webrt/SelfService/Display.html
new file mode 100755 (executable)
index 0000000..2d44f14
--- /dev/null
@@ -0,0 +1,190 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/SelfService/Attic/Display.html,v 1.1 2002-08-12 06:17:09 ivan Exp $
+%# Copyright 1996-2001 Jesse Vincent <jesse@fsck.com>
+
+<& /SelfService/Elements/Header, Title => 'Display ticket #'.$Ticket->id &>
+
+
+<& /Elements/ListActions, actions => \@results &>
+
+<TABLE>
+    <TR>
+       <TD VALIGN=TOP WIDTH="20%" ALIGN=RIGHT>
+       Ticket Id
+       </TD>
+      <TD>
+       <%$Ticket->Id%>
+       </TD>
+      </TR>
+    <TR>       
+      <TD VALIGN=TOP WIDTH="20%" ALIGN=RIGHT>
+       Requestors
+       </TD>
+      <TD>
+       <%$Ticket->RequestorsAsString%>
+      </TD>
+      </TR>
+    <TR>       
+      <TD VALIGN=TOP WIDTH="20%" ALIGN=RIGHT>
+       Cc
+       </TD>
+      <TD>
+       <%$Ticket->CcAsString%>
+      </TD>
+    </TR>
+
+  <TR> 
+      <TD VALIGN=TOP WIDTH="20%" ALIGN=RIGHT>
+       Status
+       </TD>
+      <TD>
+       <%$Ticket->Status%>
+      </TD>
+    </TR>
+
+    <TR>       
+      <TD VALIGN=TOP WIDTH="20%" ALIGN=RIGHT>
+       Queue
+       </TD>
+      <TD>
+       <%$Ticket->QueueObj->Name%> (<%$Ticket->QueueObj->Description%>)
+      </TD>
+    </TR>
+    <TR>       
+      <TD VALIGN=TOP WIDTH="20%" ALIGN=RIGHT>
+       Priority
+      </TD>
+      <TD>
+       <%$Ticket->Priority %>
+      </TD>
+    </TR>
+%  if ($Ticket->TimeWorked) {
+   <TR>        
+      <TD VALIGN=TOP WIDTH="20%" ALIGN=RIGHT>
+       Worked
+      </TD>
+      <TD>
+       <%$Ticket->TimeWorked %> minutes
+      </TD>
+    </TR>
+% }
+    
+% my $selects = $Ticket->QueueObj->KeywordSelects;
+% while (my $select = $selects->Next) {
+    <TR>
+      <TD VALIGN=TOP WIDTH="20%" ALIGN=RIGHT>
+       <%$select->Name%>
+      </TD>
+      <TD>
+% my $object_keywords = $Ticket->KeywordsObj($select->id);     
+% while (my $keyword = $object_keywords->Next) {
+       <%$keyword->KeywordObj->RelativePath($select->KeywordObj)%>
+% }
+%}
+       </TD>
+      </TR>
+
+
+
+
+       </TABLE>
+<TABLE BORDER=0 CELLSPACING=0>
+% my ($i);
+%while (my $Transaction = $Transactions->Next) {
+% $i++;
+%      if ($Transactions->IsLast) {
+       <a name="lasttrans"></a>
+%      }
+       <& /Ticket/Elements/ShowTransaction, Transaction => $Transaction, 
+                                            RowNum => $i,
+                                            Ticket => $Ticket &>
+
+%}
+</TABLE>
+
+
+<%INIT>
+
+my ($field, @results);
+
+# {{{ Load the ticket
+#If we get handed two ids, mason will make them an array. bleck.
+# We want teh first one. Just because there's no other sensible way
+# to deal
+my @id = (ref $id eq 'ARRAY') ? @{$id} : ($id);                
+
+
+my $Ticket = new RT::Ticket($session{'CurrentUser'});
+if ($id[0] eq 'new') {
+    # {{{ Create a new ticket
+    
+    my $Queue = new RT::Queue($session{'CurrentUser'});        
+    unless ($Queue->Load($ARGS{'Queue'})) {
+       $m->comp('Error.html', Why => 'Queue not found');
+       $m->abort;
+    }
+    
+    unless ($Queue->CurrentUserHasRight('CreateTicket')) {
+       $m->comp('Error.html', Why => 'You have no permission to create tickets in that queue.');
+       $m->abort; 
+    }
+    
+    my @Requestors = split(/,/,$ARGS{'Requestors'});
+    my @Cc = split(/,/,$ARGS{'Cc'});
+    
+
+    my $MIMEObj = MakeMIMEEntity ( Subject => $ARGS{'Subject'},
+                                  From => $ARGS{'From'},
+                                  Cc => $ARGS{'Cc'},
+                                  Body => $ARGS{'Content'},
+                                  AttachmentFieldName => 'Attach');
+
+    #TODO in Create_Details.html: priorities and due-date      
+    my ($id, $Trans, $ErrMsg)= $Ticket->Create(Queue=>$ARGS{Queue},
+                                              Requestor=> \@Requestors,
+                                              Cc => \@Cc,
+                                              Subject=>$ARGS{Subject},
+                                              MIMEObj => $MIMEObj        
+                                             );         
+    unless ($id && $Trans) {
+       $m->comp('Error.html', Why => $ErrMsg);
+       $m->abort(); 
+    }
+
+    push(@results, $ErrMsg);
+
+    # }}}
+}
+else {
+    unless ($Ticket->Load($id[0])) {
+       $m->comp('Error.html', Why =>"Couldn't load ticket '$id'");
+       $m->abort();
+    }
+}
+# }}}
+
+unless ($session{'CurrentUser'}->HasQueueRight ( TicketObj => $Ticket,
+                                                Right => 'ShowTicket')) {
+    $m->comp('Error.html', Why => "No permission to display that ticket");
+    $m->abort();
+}
+
+my ($code, $msg);
+
+#Update the status
+if ((defined $ARGS{'Status'}) and 
+    ($ARGS{'Status'} ne $Ticket->Status)) {
+    ($code, $msg) = $Ticket->SetStatus($ARGS{'Status'});
+    push @results, "$msg";
+}
+
+ProcessUpdateMessage(ARGSRef=>\%ARGS, Actions=>\@results, TicketObj=>$Ticket);
+
+my $Transactions = $Ticket->Transactions;
+
+</%INIT>
+
+
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/rt/webrt/SelfService/Elements/GotoTicket b/rt/webrt/SelfService/Elements/GotoTicket
new file mode 100755 (executable)
index 0000000..0c0c8b6
--- /dev/null
@@ -0,0 +1 @@
+<FORM ACTION="<%$RT::WebPath%>/SelfService/Display.html"><input type=submit value="Goto ticket">&nbsp;<input size=4 name=id></FORM>
diff --git a/rt/webrt/SelfService/Elements/Header b/rt/webrt/SelfService/Elements/Header
new file mode 100755 (executable)
index 0000000..ecf58f4
--- /dev/null
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
+     "http://www.w3.org/TR/REC-html40/loose.dtd"> 
+<HTML>
+<HEAD>
+<TITLE><%$Title%></TITLE>
+% if ($Code) {
+<META NAME="HTTP-EQUIV" VALUE="<%$Code%> <%$Why%>">
+% }
+
+<link rel="stylesheet" href="<%$RT::WebPath%>/NoAuth/webrt.css" type="text/css">
+</HEAD>
+<BODY BGCOLOR="<%$BgColor%>">
+<TABLE BORDER=0 WIDTH=100% CELLSPACING=0 BGCOLOR="#993333">
+<TR VALIGN=TOP>
+<TD WIDTH=32>
+        <IMG SRC="<%$RT::LogoURL%>" alt="RT">
+</TD>
+<TD VALIGN=CENTER ALIGN=LEFT>
+<font size=+2 color=#ffffff>
+<B>
+<%$Title%>
+</B>
+</font>
+</TD>
+<TD ALIGN=RIGHT>
+<font color="#ffffff">
+% if ($session{'CurrentUser'} ) {
+Signed in as <b><%$session{'CurrentUser'}->Name%></b>.<BR>
+% if ($session{'CurrentUser'}->HasSystemRight('ModifySelf')) {
+[<A class='inverse' HREF="<%$RT::WebPath%>/SelfService/Prefs.html" >Preferences</A>]
+% }
+% unless ($RT::WebExternalAuth) { 
+ [<A class='inverse' HREF="<%$RT::WebPath%>/NoAuth/Logout.html">Logout</a>]
+% }
+% } else {
+Not logged in.
+% }
+</font>
+</TD>
+</TR>
+</TABLE>
+
+<BR>
+<& /SelfService/Elements/Tabs &>
+
+<%ARGS>
+$Title => ''
+$Code => undef
+$Why => undef
+$BgColor => '#ffffff'
+</%ARGS>
+<%INIT>
+$Title = "RT/$RT::rtname: ".$Title;
+</%INIT>
+
diff --git a/rt/webrt/SelfService/Elements/MyRequests b/rt/webrt/SelfService/Elements/MyRequests
new file mode 100644 (file)
index 0000000..ce268d5
--- /dev/null
@@ -0,0 +1,41 @@
+<& /Elements/TitleBoxStart, title => "Your $friendly_status requests" &>
+<TABLE BORDER=0 cellspacing=1 cellpadding=1 BGCOLOR="#eeeeee" WIDTH=100%>
+<TR>
+<TH>Subject</TH>
+<TH>Status</TH>
+<TH>Owner</TH>
+<TH>&nbsp;</TH>
+</TR>
+<TR>
+% while (my $Ticket = $MyTickets->Next) {
+<TR>
+<TD>
+<%$Ticket->Id%>: <%$Ticket->Subject%>
+</TD>
+<TD>
+<%$Ticket->Status%>
+</TD><TD>
+<%$Ticket->OwnerObj->Name%>
+</TD><TD ALIGN=RIGHT>
+[<A HREF="<% $RT::WebPath %>/SelfService/Display.html?id=<%$Ticket->Id%>">Details</A>]
+</TD>
+</TR>
+% }
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+
+
+<%INIT>
+my $MyTickets;
+$MyTickets = new RT::Tickets ($session{'CurrentUser'});
+$MyTickets->LimitRequestor(VALUE => $session{'CurrentUser'}->EmailAddress);
+
+foreach my $status (@status) {
+
+        $MyTickets->LimitStatus(VALUE => $status);
+}
+</%INIT>
+<%ARGS>
+$friendly_status => 'open'
+@status => ('open', 'new', 'stalled')
+</%ARGS>
diff --git a/rt/webrt/SelfService/Elements/Tabs b/rt/webrt/SelfService/Elements/Tabs
new file mode 100644 (file)
index 0000000..d689d8a
--- /dev/null
@@ -0,0 +1,49 @@
+<TABLE WIDTH=100%>
+<TR>
+% foreach $tab (sort keys %{$tabs}) {
+<TD ALIGN=CENTER>
+[<A HREF="<%$RT::WebPath%>/<% $tabs->{"$tab"}->{'path'}%>"><% $tabs->{"$tab"}->{'title'}%></A>]
+</TD>
+%}
+
+% if ($actions) {
+
+<TD ALIGN=RIGHT>
+<TABLE><TR>
+%  foreach my $action (sort keys %{$actions}) {
+<TD>
+<FONT SIZE=-1>
+% if ($actions->{"$action"}->{'html'}) {
+<%$actions->{"$action"}->{'html'} |n%>
+% } else {
+<A HREF="<%$RT::WebPath%>/<% $actions->{$action}->{'path'}%>"><% $actions->{$action}->{'title'}%></A>
+% }
+</FONT>
+</TD>
+%  }
+</TR>
+</TABLE>
+</TD>
+%}
+</TR>
+</TABLE>
+<hr>
+<%INIT>
+my ($tab);
+my $tabs = { A  => { title => 'Open requests',
+                        path => 'SelfService/',
+                      },
+             B => { title => 'Closed requests',
+                         path => 'SelfService/Closed.html',
+                       },
+             C => { title => 'New request',
+                    path => 'SelfService/Create.html'
+                    }
+           };
+my $actions = {
+       B => { html => $m->scomp('GotoTicket') 
+               }
+       };
+</%INIT>
+
+
diff --git a/rt/webrt/SelfService/Error.html b/rt/webrt/SelfService/Error.html
new file mode 100755 (executable)
index 0000000..19b79e6
--- /dev/null
@@ -0,0 +1,22 @@
+<& /SelfService/Elements/Header, Title => 'Error' &>
+<& /Elements/TitleBoxStart, title => $Title &>
+<%$Why%>
+<br>
+<font size=-1>
+<%$Details%>
+</font>
+<& /Elements/TitleBoxEnd &>
+</body>
+</HTML>
+
+
+<%args>
+$Code => undef
+$Details => undef
+$Title => "RT Error"
+$Why => "the calling component did not specify why"
+</%args>
+
+<%INIT>
+$RT::Logger->error("WebRT: $Why ($Details)");
+</%INIT>
diff --git a/rt/webrt/SelfService/Prefs.html b/rt/webrt/SelfService/Prefs.html
new file mode 100755 (executable)
index 0000000..9c614e9
--- /dev/null
@@ -0,0 +1,51 @@
+<& /SelfService/Elements/Header, title => 'Preferences' &>
+
+<& /Elements/ListActions, actions => \@results &>
+<form method=post>
+
+% unless ($RT::WebExternalAuth) {
+<& /Elements/TitleBoxStart, title => 'Change password'  &>
+New password: <input type=password name="NewPass1" size=16>
+Confirm: <input type=password name="NewPass2" size=16>
+<& /Elements/TitleBoxEnd &>
+<BR>
+% }
+<& /Elements/TitleBoxStart, title => 'Signature'  &>
+
+<TEXTAREA COLS=72 ROWS=4 WRAP=HARD NAME="Signature"><% $session{'CurrentUser'}->UserObj->Signature %></TEXTAREA>
+<br>
+<BR>
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit &>
+         </form>
+
+
+<%INIT>
+my @results;
+
+if ($NewPass1) {
+    if ($NewPass1 ne $NewPass2) {
+       push (@results, "Passwords did not match.");
+    }  
+    else {
+       my ($val, $msg)=$session{'CurrentUser'}->UserObj->SetPassword($NewPass1);
+       push (@results, "Password: ".$msg);
+    }  
+}
+if ($Signature) {
+    $Signature =~ s/(\r\n|\r)/\n/g;
+    if ($Signature ne $session{'CurrentUser'}->UserObj->Signature) {
+       my ($val, $msg)=$session{'CurrentUser'}->UserObj->SetSignature($Signature);
+       push (@results, "Signature: ".$msg);
+    }
+}
+#A hack to make sure that session gets rewritten.
+
+$session{'i'}++;
+</%INIT>
+
+<%ARGS>
+$Signature => undef
+$NewPass1 => undef
+$NewPass2 => undef
+</%ARGS>
diff --git a/rt/webrt/SelfService/Update.html b/rt/webrt/SelfService/Update.html
new file mode 100755 (executable)
index 0000000..17f1618
--- /dev/null
@@ -0,0 +1,40 @@
+<& /SelfService/Elements/Header, Title => 'Update ticket #'.$Ticket->id &>
+
+
+<FORM ACTION="Display.html" METHOD=POST ENCTYPE="multipart/form-data">
+
+Status:
+<& /Elements/SelectStatus, Name=>"Status", Default => $DefaultStatus &> 
+<input type=hidden name="UpdateType" value="response">
+
+Subject: <input name="UpdateSubject" size=60 value="Re: <% $Ticket->Subject %>"> <br>
+Attach: <input name="UpdateAttachment" type=file><br>
+<& /Elements/MessageBox, Name=>"UpdateContent", QuoteTransaction=>$ARGS{QuoteTransaction} &>
+               <INPUT TYPE=HIDDEN NAME=id VALUE="<%$Ticket->Id%>"><br>
+
+
+<& /Elements/Submit &>
+  </FORM>
+
+
+
+<%INIT>
+
+my $Ticket = LoadTicket($id);
+
+my $title = "Update ticket #" . $Ticket->id;
+
+$DefaultStatus = $Ticket->Status() unless ($DefaultStatus);
+
+
+Abort("No permission to view update ticket") 
+       unless ( $Ticket->CurrentUserHasRight('ReplyToTicket') or
+                     $Ticket->CurrentUserHasRight('ModifyTicket') ); 
+
+</%INIT>
+
+<%ARGS>
+$id => undef
+$Action => undef
+$DefaultStatus => undef
+</%ARGS>
diff --git a/rt/webrt/SelfService/index.html b/rt/webrt/SelfService/index.html
new file mode 100644 (file)
index 0000000..a377d8c
--- /dev/null
@@ -0,0 +1,3 @@
+<& /SelfService/Elements/Header, title => 'Self Service' &>
+
+<& /SelfService/Elements/MyRequests &>
diff --git a/rt/webrt/Ticket/Attachment/dhandler b/rt/webrt/Ticket/Attachment/dhandler
new file mode 100644 (file)
index 0000000..0d646cc
--- /dev/null
@@ -0,0 +1,27 @@
+<%perl>
+     my ($ticket, $trans,$attach, $filename);
+     my $arg = $m->dhandler_arg;                # get rest of path
+     if ($arg =~ '^(\d+)/(\d+)') {
+        $trans = $1;
+        $attach = $2;
+     }
+    else {
+        Abort("Corrupted attachment URL.");
+        }
+     my $AttachmentObj = new RT::Attachment($session{'CurrentUser'});
+     $AttachmentObj->Load($attach) || Abort("Attachment '$attach' could not be loaded");
+
+
+     unless ($AttachmentObj->id) {
+        Abort("Bad attachment id. Couldn't find attachment '$attach'\n");
+    }
+     unless ($AttachmentObj->TransactionId() == $trans ) {
+        Abort("Bad transaction number for attachment. $trans should be".$AttachmentObj->TransactionId() ."\n");
+
+     }
+     my $content_type = $AttachmentObj->ContentType || 'text/plain';
+     SetContentType($content_type);
+     $m->out($AttachmentObj->Content); 
+     $m->abort; 
+</%perl>
+
diff --git a/rt/webrt/Ticket/Create.html b/rt/webrt/Ticket/Create.html
new file mode 100755 (executable)
index 0000000..2c61de0
--- /dev/null
@@ -0,0 +1,199 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Ticket/Attic/Create.html,v 1.1 2002-08-12 06:17:09 ivan Exp $
+%# Copyright 1996-2000 Jesse Vincent <jesse@fsck.com> 
+
+<& /Elements/Header, Title => "Create a new ticket" &>
+<& /Elements/Tabs, current_toptab => "Ticket/Create.html" &>
+<FORM ACTION="Display.html" METHOD="POST" ENCTYPE="multipart/form-data">
+<INPUT TYPE=HIDDEN Name="id" VALUE="new">
+<A NAME="top">
+       
+       
+[<a class="currenttab">Show basics</a>] [<A HREF="#detail">Show details</a>]
+<BR>
+<& /Elements/TitleBoxStart, contentbg => "#cccccc", title => "Create a new ticket"&>
+<div align=right><input type=submit value="Create"></div>
+<TABLE border=0 cellpadding=0 cellspacing=0>
+<TR><TD>Queue</TD>
+<TD><% $QueueObj->Name %>
+<INPUT TYPE=HIDDEN NAME=Queue Value="<%$QueueObj->Name%>">
+</TD>
+<TD>Status:
+</TD>
+<TD>
+<& /Elements/SelectStatus, Name => "Status", Default=> 'new' &>
+</TD>
+<TD>
+Owner: 
+</TD>
+<TD>
+<& /Elements/SelectOwner, Name => "ValueOfOwner", QueueObj => $QueueObj &>
+</TD>
+</TR>
+<TR>
+<TD>
+Requestors: 
+</TD>
+<TD COLSPAN=5>
+<INPUT Name="Requestors" Value="<%$session{CurrentUser}->EmailAddress%>" SIZE=40>
+</TD>
+</TR>
+<TR>
+<TD>
+Cc:
+</TD>
+<TD COLSPAN=5>
+ <INPUT NAME="Cc" SIZE=40>
+</TD>
+</TR>
+<TR>
+<TD>
+Admin Cc:
+</TD>
+<TD COLSPAN=5>
+ <INPUT NAME="AdminCc" SIZE=40>
+</TD>
+</TR>
+<TR>
+<TD>
+Subject:
+</TD>
+<TD COLSPAN=5>
+<INPUT Name="Subject" SIZE=60 MAXSIZE=100 value="">
+</TD>
+</TR>
+<TR>
+<TD>
+Attach file:
+</TD>
+<TD COLSPAN=5>
+<INPUT TYPE=FILE NAME="Attach">
+</TD>
+</TR>
+<TR>
+<TD COLSPAN=6>
+Describe the issue below:<br>
+<& /Elements/MessageBox, QuoteTransaction => $QuoteTransaction &>
+
+<BR>
+</TD>
+</TR>
+<TR>
+<TD ALIGN=RIGHT COLSPAN=2>
+</TD>
+</TR>
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit, Label => "Create"&>
+
+<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+
+<A NAME="detail">
+       [<A HREF="#top">Show basics</a>] [<a class="currenttab">Show details</a>]
+<BR>
+<TABLE WIDTH="100%" BORDER=0>
+<TR>
+<TD WIDTH="50%" VALIGN=TOP>
+
+         <& /Elements/TitleBoxStart, title => 'The Basics', 
+               title_class=> 'inverse',  
+               color => "#993333" &>
+<TABLE BORDER=0>
+<TR><TD ALIGN=RIGHT>Priority:</TD><TD><input size=3 name="InitialPriority" value="<%$QueueObj->InitialPriority%>"></TD></TR>
+<TR><TD ALIGN=RIGHT>Final Priority:</TD><TD><input size=3 name="FinalPriority" value="<%$QueueObj->FinalPriority%>"></TD></TR>
+<TR><TD ALIGN=RIGHT>Time Worked:</TD><TD><input size=3 name="TimeWorked"></TD></TR>
+<TR><TD ALIGN=RIGHT>Time Left:</TD><TD><input size=3 name="TimeLeft"></TD></TR>
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+<BR>
+<BR>
+
+
+ <& /Elements/TitleBoxStart, 
+               title_class=> 'inverse',  
+               title => "Keyword Selections", color => "#993300"
+  &>
+<TABLE BORDER=0>
+% while ( my $KeywordSelect = $KeywordSelects->Next ) {
+%   my $Descendents = $KeywordSelect->KeywordObj->Descendents;
+     <TR><TD ALIGN=RIGHT>
+       <% $KeywordSelect->Name %></TD><TD>
+         <INPUT TYPE="hidden" NAME="KeywordSelectMagic<% $KeywordSelect->id %>" VALUE="1">
+           <SELECT NAME="KeywordSelect-<% $KeywordSelect->id %>"
+             <% $KeywordSelect->Single ? "" : " MULTIPLE " %> SIZE=5>
+%#
+%#  All of this cruft is so we have a 'no keyword' selector for single
+%#  keywords that's only selected when there's no value.
+%
+% foreach my $kid ( keys %{$Descendents} ) {
+             <OPTION VALUE="<% $kid %>"><% $Descendents->{$kid} %></OPTION>
+%   }
+%   if ( $KeywordSelect->Single) {
+<OPTION VALUE="" SELECTED>(empty)</OPTION>
+% }
+           </SELECT>
+      </TD></TR>
+% }
+  
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+
+</TD>
+
+<TD VALIGN="TOP">
+<& /Elements/TitleBoxStart, title => "Dates",
+               title_class=> 'inverse',  
+                color => "#663366" &>
+
+<TABLE BORDER=0>
+<TR><TD ALIGN=RIGHT>Starts:</TD><TD><input size=10 name="Starts"></TD></TR>
+<TR><TD ALIGN=RIGHT>Due:</TD><TD><input size=10 name="Due"></TD></TR>
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+<BR>
+<& /Elements/TitleBoxStart, title => 'Relationships', 
+       title_class=> 'inverse',  
+       titleright => '', color=> "#336633" &>
+
+<i>(Enter ticket ids or URLs, seperated with spaces)</i>
+<TABLE BORDER=0>
+<TR><TD ALIGN=RIGHT>Depends on</TD><TD><input size=10 name="new-DependsOn"></TD></TR>
+<TR><TD ALIGN=RIGHT>Depended on by</TD><TD><input size=10 name="DependsOn-new"></TD></TR>
+<TR><TD ALIGN=RIGHT>Parents</TD><TD><input size=10 name="new-MemberOf"></TD></TR>
+<TR><TD ALIGN=RIGHT>Children</TD><TD><input size=10 name="MemberOf-new"></TD></TR>
+<TR><TD ALIGN=RIGHT>Refers to</TD><TD><input size=10 name="new-RefersTo"></TD></TR>
+<TR><TD ALIGN=RIGHT>Referred to by</TD><TD><input size=10 name="RefersTo-new"></TD></TR>
+
+
+</TABLE>
+<& /Elements/TitleBoxEnd &>
+<BR>
+
+</TD>
+</TR>
+</TABLE>
+<& /Elements/Submit, Label => "Create"&>
+</FORM>
+<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+<BR><BR><BR><BR><BR><BR><BR><BR><BR><BR><BR>
+
+<%INIT>
+my $QueueObj = new RT::Queue($session{'CurrentUser'});
+$QueueObj->Load($Queue) || Abort("Queue could not be loaded.");
+my $KeywordSelects = $QueueObj->KeywordSelects;
+
+</%INIT>
+
+<%ARGS>
+$DependsOn => undef
+$DependedOnBy => undef
+$MemberOf => undef
+$QuoteTransaction => undef
+$Queue => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Display.html b/rt/webrt/Ticket/Display.html
new file mode 100755 (executable)
index 0000000..cb0dc25
--- /dev/null
@@ -0,0 +1,152 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Ticket/Attic/Display.html,v 1.1 2002-08-12 06:17:09 ivan Exp $
+%# Copyright 1996-2000 Jesse Vincent <jesse@fsck.com>
+
+<& /Elements/Header, Title => "Ticket #".$Ticket->Id ." ".$Ticket->Subject &>
+<& /Ticket/Elements/Tabs, Ticket => $Ticket, current_tab => 'Ticket/Display.html?id='.$Ticket->id &>
+
+<& /Elements/ListActions, actions => \@Actions &>
+
+<& /Ticket/Elements/ShowSummary,  Ticket => $Ticket &>
+
+
+<BR>
+<& /Ticket/Elements/ShowHistory , 
+      Ticket => $Ticket, 
+      Collapsed => $ARGS{'Collapsed'}, 
+      ShowHeaders => $ARGS{'ShowHeaders'} &> 
+
+  
+<%ARGS>
+$id => undef
+$Create => undef
+$ShowHeaders => undef
+$Collapsed => undef
+</%ARGS>
+
+<%INIT>
+
+  
+  my ($linkid, $message, $tid, $Ticket, @Actions);  
+
+$Ticket = new RT::Ticket($session{'CurrentUser'});
+
+unless ($id) {
+    Abort('No ticket specified');
+}
+
+if ($ARGS{'id'} eq 'new') {
+    # {{{ Create a new ticket
+    
+    my $Queue = new RT::Queue($session{'CurrentUser'});        
+    unless ($Queue->Load($ARGS{'Queue'})) {
+       Abort('Queue not found');
+    }
+    
+    unless ($Queue->CurrentUserHasRight('CreateTicket')) {
+       Abort('You have no permission to create tickets in that queue.');
+    }
+   
+    my $due = new RT::Date($session{'CurrentUser'});
+    $due->Set(Format => 'unknown', Value => $ARGS{'Due'});
+    my $starts = new RT::Date($session{'CurrentUser'});
+    $starts->Set(Format => 'unknown', Value => $ARGS{'Starts'});
+    
+    my @Requestors = split(/,/,$ARGS{'Requestors'});
+    my @Cc = split(/,/,$ARGS{'Cc'});
+    my @AdminCc = split(/,/,$ARGS{'AdminCc'});
+    
+    my $MIMEObj = MakeMIMEEntity( Subject => $ARGS{'Subject'},
+                                 From => $ARGS{'From'},
+                                 Cc => $ARGS{'Cc'},
+                                 Body => $ARGS{'Content'},
+                                 AttachmentFieldName => 'Attach');
+                                 
+    my %create_args = ( 
+                      Queue=>$ARGS{Queue},
+                      Owner=>$ARGS{ValueOfOwner},
+                      InitialPriority=> $ARGS{InitialPriority},
+                      FinalPriority=> $ARGS{FinalPriority},
+                      TimeLeft => $ARGS{TimeLeft},
+                      TimeWorked => $ARGS{TimeWorked},
+                      Requestor=> \@Requestors,
+                      Cc => \@Cc,
+                      AdminCc => \@AdminCc,
+                      Subject=>$ARGS{Subject},
+                      Status=>$ARGS{Status},
+                      Due => $due->ISO,
+                      Starts => $starts->ISO,
+                      MIMEObj => $MIMEObj        
+                     );         
+
+    
+    # we need to get any KeywordSelect-<integer> fields into %create_args..
+    grep { $_ =~ /^KeywordSelect-/ && {$create_args{$_} = $ARGS{$_}}} %ARGS;
+
+    my ($id, $Trans, $ErrMsg)= $Ticket->Create(%create_args);
+    unless ($id && $Trans) {
+       Abort($ErrMsg);
+    }
+    my @linktypes = qw( DependsOn MemberOf RefersTo );
+    
+    foreach my $linktype (@linktypes) {
+      foreach my $luri (split (/ /,$ARGS{"new-$linktype"})) {
+       $luri =~ s/\s*$//; # Strip trailing whitespace
+       my ($val, $msg) = $Ticket->AddLink( Target => $luri,
+                                           Type => $linktype);
+       push @Actions, $msg;
+      }
+      
+      foreach my $luri (split (/ /,$ARGS{"$linktype-new"})) {
+       my ($val, $msg) = $Ticket->AddLink( Base => $luri,
+                                           Type => $linktype);
+       
+       push @Actions, $msg;
+      }
+    }
+    # don't try to change queue to the current queue
+    delete $ARGS{'Queue'};
+
+    push(@Actions, $ErrMsg);
+    unless ($Ticket->CurrentUserHasRight('ShowTicket')) {
+      Abort("No permission to view newly created ticket #".$Ticket->id.".");
+    }
+    # }}}
+}
+
+else { 
+    $Ticket = LoadTicket($ARGS{'id'});
+    unless ($Ticket->CurrentUserHasRight('ShowTicket')) {
+       Abort("No permission to view ticket");
+    }
+
+
+if (defined $ARGS{'Action'}) {
+  if ($ARGS{'Action'} =~ /^(Steal|Kill|Take|SetTold)$/) {
+    my $action = $1;
+    my ($res, $msg)=$Ticket->$action();
+    push(@Actions, $msg);
+  }
+}
+    $ARGS{'UpdateContent'} =~ s/\r\n/\n/g;
+
+    if ($ARGS{'UpdateContent'} &&
+        $ARGS{'UpdateContent'} ne '' &&
+        $ARGS{'UpdateContent'} ne  "-- \n" .
+                                $session{'CurrentUser'}->UserObj->Signature
+       ) {
+           ProcessUpdateMessage(ARGSRef=>\%ARGS, 
+                                Actions=>\@Actions, 
+                                TicketObj=>$Ticket);
+       }
+#Process status updates
+my @BasicActions = ProcessTicketBasics(ARGSRef => \%ARGS, TicketObj=>$Ticket);
+
+push (@Actions, @BasicActions);
+}
+</%INIT>
+
+
+
+
diff --git a/rt/webrt/Ticket/Elements/AddWatchers b/rt/webrt/Ticket/Elements/AddWatchers
new file mode 100755 (executable)
index 0000000..053cff1
--- /dev/null
@@ -0,0 +1,54 @@
+<BR>
+<%$msg%><br>
+
+Add new watchers:<br>
+
+<table>
+% if ($Users) {
+<tr><td>
+Type
+</td><td>
+Username
+</td></tr>
+% while (my $u = $Users->Next ) {
+<tr><td><&/Elements/SelectWatcherType, Name => "WatcherTypeUser".$u->Id &></td><td><%$u->Name%> (<%$u->RealName%>)</td></tr>
+% }
+% }
+
+<tr><td>
+Type
+</td><td>
+Email
+</td></tr>
+<tr><td>
+<&/Elements/SelectWatcherType, Name => "WatcherTypeEmail1" &>
+</td><td>
+<input name="WatcherAddressEmail1" size=15>
+</td></tr>
+<tr><td>
+<&/Elements/SelectWatcherType, Name => "WatcherTypeEmail2" &> 
+</td><td>
+<input name="WatcherAddressEmail2" size=15>
+</td></tr>
+<tr><td>
+<&/Elements/SelectWatcherType, Name => "WatcherTypeEmail3" &>
+</td><td>
+<input name="WatcherAddressEmail3" size=15>
+</td></tr>
+</table>
+
+<%INIT>
+my ($msg, $Users);
+if ($UserString) {
+    $Users = new RT::Users($session{'CurrentUser'});
+    $Users->Limit(FIELD => $UserField,
+                 VALUE => $UserString,
+                 OPERATOR => $UserOp);
+     }
+</%INIT>
+
+<%ARGS>
+$UserField => 'Name'
+$UserOp => '='
+$UserString => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/EditBasics b/rt/webrt/Ticket/Elements/EditBasics
new file mode 100755 (executable)
index 0000000..1214287
--- /dev/null
@@ -0,0 +1,62 @@
+<TABLE>
+<TR>
+<TD COLSPAN=6>
+        Subject<BR>
+        <input name=Subject value="<%$TicketObj->Subject|h%>"  SIZE=50>
+</TD>
+</TR>
+<TR>
+<TD>
+<& /Elements/ShadedBox, 
+        title => 'Status',
+        content => $SelectStatus
+&>
+</TD>
+<TD>
+
+<& /Elements/ShadedBox,
+       title => 'Time Worked',
+       content => "<input name=TimeWorked value=\"".$TicketObj->TimeWorked."\" SIZE=5>" 
+&>
+
+</TD>
+<TD>
+<& /Elements/ShadedBox,
+       title => 'Time Left',
+       content => "<input name=TimeLeft value=\"".$TicketObj->TimeLeft."\" SIZE=5>" 
+&>
+</TD>
+<TD>
+<& /Elements/ShadedBox,
+       title => 'Priority',
+       content => "<input name=Priority value=\"".$TicketObj->Priority."\" SIZE=3>"
+&>
+
+</TD>
+<TD>
+<& /Elements/ShadedBox,
+       title => 'Final Priority',
+       content => "<input name=FinalPriority value=\"".$TicketObj->FinalPriority."\" SIZE=3>"
+&>
+
+
+</TD>
+<TD>
+<& /Elements/ShadedBox,
+        title => 'Queue',
+        content => "$SelectQueue"
+ &>
+</TD>
+</TR>
+</TABLE>
+
+<%INIT>
+#It's hard to do this inline, so we'll preload the html of the selectstatus in here.
+my $SelectStatus = $m->scomp("/Elements/SelectStatus", Name => 'Status', Default=> $TicketObj->Status);
+my $SelectQueue = $m->scomp("/Elements/SelectQueue", Name => 'Queue', Default =>$TicketObj->QueueObj->Id);
+
+</%INIT>
+<%ARGS>
+
+$TicketObj => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/EditDates b/rt/webrt/Ticket/Elements/EditDates
new file mode 100755 (executable)
index 0000000..f04130b
--- /dev/null
@@ -0,0 +1,46 @@
+<TABLE>
+<TR>
+<TD>
+Starts: 
+</TD>
+<TD>
+<& /Elements/SelectDate, menu_prefix => 'Starts', current => 0 &> 
+        (<% $TicketObj->StartsObj->AsString %>)
+</TD>
+</TR>
+<TR>
+<TD>
+Started:
+</TD>
+<TD>
+<& /Elements/SelectDate, menu_prefix => 'Started', current => 0 &> (<%$TicketObj->StartedObj->AsString %>)
+
+
+
+</TD>
+</TR>
+
+<TR>
+<TD>
+Last Contact:
+</TD>
+<TD>
+<& /Elements/SelectDate, menu_prefix => 'Told', current => 0 &> (<% $TicketObj->ToldObj->AsString %>)
+
+</TD>
+</TR>
+<TR>
+<TD>
+Due:
+</TD>
+<TD>
+
+<& /Elements/SelectDate, menu_prefix => 'Due', current => 0 &> (<% $TicketObj->DueObj->AsString %>)
+</TD>
+</TR>
+
+</TABLE>
+<%ARGS>
+$TicketObj => undef
+</%ARGS>
+
diff --git a/rt/webrt/Ticket/Elements/EditKeywordSelects b/rt/webrt/Ticket/Elements/EditKeywordSelects
new file mode 100644 (file)
index 0000000..34ade9f
--- /dev/null
@@ -0,0 +1,45 @@
+
+<TABLE>
+    <TR>
+% while ( my $KeywordSelect = $KeywordSelects->Next ) {
+% my $CurrentKeywords = $TicketObj->KeywordsObj($KeywordSelect->id);   
+%   my $Descendents = $KeywordSelect->KeywordObj->Descendents;
+      <TD VALIGN=TOP>
+       <% $KeywordSelect->Name %>
+       <BR>
+         <INPUT TYPE="hidden" NAME="KeywordSelectMagic<% $KeywordSelect->id %>" VALUE="1">
+           <SELECT NAME="KeywordSelect<% $KeywordSelect->id %>"
+             <% $KeywordSelect->Single ? "" : " MULTIPLE " %> SIZE=5>
+%#
+%#
+%#  All of this cruft is so we have a 'no keyword' selector for single
+%#  keywords that's only selected when there's no value.
+%
+% my $selected_keywords = 0;
+% foreach my $kid ( keys %{$Descendents} ) {
+% my $selected = 0;
+% if ($CurrentKeywords->HasEntry($kid)) { $selected_keywords++; $selected=1;}
+             <OPTION VALUE="<% $kid %>" 
+               <% $selected && 'SELECTED'%>>
+               <% $Descendents->{$kid} %>
+             </OPTION>
+%   }
+%   if ( $KeywordSelect->Single) {
+<OPTION VALUE="" <% ($selected_keywords == 0) && 'SELECTED' %> >(empty)</OPTION>
+% }
+           </SELECT>
+      </TD>
+% }
+    </TR>
+  
+</TABLE>
+
+
+<%INIT>
+my $KeywordSelects = $TicketObj->QueueObj->KeywordSelects;
+</%INIT>
+
+<%ARGS>
+$TicketObj => undef
+</%ARGS>
+
diff --git a/rt/webrt/Ticket/Elements/EditLinks b/rt/webrt/Ticket/Elements/EditLinks
new file mode 100755 (executable)
index 0000000..b0296fc
--- /dev/null
@@ -0,0 +1,109 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Ticket/Elements/Attic/EditLinks,v 1.1 2002-08-12 06:17:09 ivan Exp $
+%# Copyright 1996-2000 Jesse Vincent <jesse@fsck.com>
+
+
+<TABLE>
+<TR>
+<TD VALIGN=TOP>
+<h3>New Relationships</h3>
+<i>Enter tickets or URIs to link tickets to. Seperate multiple entries with spaces.</i><br>
+<TABLE>
+<TR><TD>Merge into:</TD><TD><input name="<%$Ticket->Id%>-MergeInto"> <i>(only one ticket)</i></TD></TR>
+<TR><TD>Depends on:</TD><TD><input name="<%$Ticket->Id%>-DependsOn"></TD></TR>
+<TR><TD>Depended on by:</TD><TD><input name="DependsOn-<%$Ticket->Id%>"></TD></TR>
+<TR><TD>Parents:</TD><TD><input name="<%$Ticket->Id%>-MemberOf"></TD></TR>
+<TR><TD>Children:</TD><TD> <input name="MemberOf-<%$Ticket->Id%>"></TD></TR>
+<TR><TD>Refers to:</TD><TD><input name="<%$Ticket->Id%>-RefersTo"></TD></TR>
+<TR><TD>Referred to by:</TD><TD> <input name="RefersTo-<%$Ticket->Id%>"></TD></TR>
+</TABLE>
+</TD>
+<TD VALIGN=TOP WIDTH=50%>
+<h3>Current Relationships</h3>
+<i>(Check boxes to delete)</i><br>
+
+Depends on:<BR>
+<UL>
+% while (my $link = $Ticket->DependsOn->Next) {
+% my $member = $link->TargetObj;
+<LI>
+<INPUT TYPE=CHECKBOX NAME="DeleteLink--<%$link->Type%>-<%$link->Target%>">
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%>
+[<%$member->Status%>]
+
+% }
+</UL>
+
+Depended on by:<BR>
+<UL>
+% while (my $link = $Ticket->DependedOnBy->Next) {
+% my $member = $link->BaseObj;
+<LI>
+<INPUT TYPE=CHECKBOX NAME="DeleteLink-<%$link->Base%>-<%$link->Type%>-">
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%> 
+[<%$member->Status%>]
+% }
+</UL>
+
+Parents:<BR>
+<UL>
+% while (my $link = $Ticket->MemberOf->Next) {
+% my $member = $link->TargetObj;
+<LI>
+<INPUT TYPE=CHECKBOX NAME="DeleteLink--<%$link->Type%>-<%$link->Target%>">
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%>
+[<%$member->Status%>]
+
+% }
+</UL>
+
+Children:<BR>
+<UL>
+% while (my $link = $Ticket->Members->Next) {
+<LI>
+<INPUT TYPE=CHECKBOX NAME="DeleteLink-<%$link->Base%>-<%$link->Type%>-">
+% my $member = $link->BaseObj;
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%> 
+[<%$member->Status%>]
+% }
+</UL>
+
+
+Refers to:<BR>
+<UL>
+% while (my $link = $Ticket->RefersTo->Next) {
+<LI>
+<INPUT TYPE=CHECKBOX NAME="DeleteLink--<%$link->Type%>-<%$link->Target%>">
+% if ($link->TargetIsLocal) {
+% my $member = $link->TargetObj;
+
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%> [<%$member->Status%>]<br>
+% } else {
+<A HREF="<%$link->TargetAsHREF%>"><%$link->Target%></A>
+% }
+%}
+</UL>
+
+Referred to by:<BR>
+<UL>
+% while (my $link = $Ticket->ReferredToBy->Next) {
+<LI>
+<INPUT TYPE=CHECKBOX NAME="DeleteLink-<%$link->Base%>-<%$link->Type%>-">
+% if ($link->BaseIsLocal) {
+% my $member = $link->BaseObj;
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%> [<%$member->Status%>]<br>
+% } else {
+<A HREF="<%$link->BaseAsHREF%>"><%$link->Base%></A>
+%}
+% }
+</UL>
+
+                           
+</TD>
+</TR>
+</TABLE>
+
+
+      
+<%ARGS>
+$Ticket => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/EditPeople b/rt/webrt/Ticket/Elements/EditPeople
new file mode 100755 (executable)
index 0000000..4f69af9
--- /dev/null
@@ -0,0 +1,37 @@
+
+<TABLE>
+<TR>
+<TD VALIGN=TOP>
+
+<h3>New watchers</h3>
+Find people whose<BR>
+<& /Elements/SelectUsers &>
+<input type=submit name="OnlySearchForPeople" value="Go!">
+
+<& AddWatchers, Ticket => $Ticket, UserString => $UserString,
+        UserOp => $UserOp, UserField => $UserField &> 
+</TD><TD VALIGN=TOP>
+<h3>Owner</h3>
+Owner: <& /Elements/SelectOwner, Name => 'Owner', QueueObj => $Ticket->QueueObj, TicketObj => $Ticket, Default => $Ticket->OwnerObj->Id &>
+<h3>Current watchers</h3>
+(Check box to delete)<br>
+
+Requestors:
+<& EditWatchers, TicketObj => $Ticket, Type => 'requestors' &>
+
+Cc:
+<& EditWatchers, TicketObj => $Ticket, Type => 'cc' &>
+
+Administrative Cc:
+<& EditWatchers, TicketObj => $Ticket, Type => 'admincc' &>
+
+</TD>
+</TR>
+</TABLE>
+
+<%ARGS>
+$UserField => undef
+$UserOp => undef
+$UserString => undef
+$Ticket => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/EditWatchers b/rt/webrt/Ticket/Elements/EditWatchers
new file mode 100755 (executable)
index 0000000..00185e8
--- /dev/null
@@ -0,0 +1,46 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Ticket/Elements/Attic/EditWatchers,v 1.1 2002-08-12 06:17:09 ivan Exp $
+%# Copyright 1996-2000 Jesse Vincent <jesse@fsck.com>
+
+<ul>
+
+%# Print out a placeholder if there are none.
+%if ($watchers->Count == 0 ) {
+<li><i>none</i>
+% }
+
+
+%while (my $watcher=$watchers->Next) {
+<li>
+<INPUT TYPE=CHECKBOX NAME="DelWatcher<%$watcher->id%>" UNCHECKED>
+%#If there's a principal backing this user, lets give a link to their
+%# account
+%if ($watcher->IsUser) { 
+<a href="<%$RT::WebPath%>/Admin/Users/Modify.html?id=<%$watcher->OwnerObj->id%>">
+<%$watcher->OwnerObj->RealName%></a>:
+%} else {
+Email address:
+%}
+<i><%$watcher->Email%></i>
+%}
+</ul>
+<%INIT>
+my ($watchers, $watcher, $set);
+if ($Type  =~ /^request/i) {
+       $watchers = $TicketObj->Requestors;
+       }
+elsif ($Type =~ /^admin/i) {
+        $watchers = $TicketObj->AdminCc;
+        }
+elsif ($Type =~ /^cc/i) {
+        $watchers = $TicketObj->Cc;
+      }
+else { $watchers = $TicketObj->Watchers;
+       }
+</%INIT>
+<%ARGS>
+$TicketObj => undef
+$Type => undef
+</%ARGS>
+
+
+
diff --git a/rt/webrt/Ticket/Elements/ShowBasics b/rt/webrt/Ticket/Elements/ShowBasics
new file mode 100755 (executable)
index 0000000..97c84c9
--- /dev/null
@@ -0,0 +1,29 @@
+ <TABLE WIDTH="100%">
+      <TR>
+       <TD VALIGN=TOP WIDTH="20%">
+         <& /Elements/ShadedBox, title => 'Id' , content => $Ticket->Id &>
+       </TD>
+       <TD VALIGN=TOP WIDTH="20%"> <& /Elements/ShadedBox, title => 'Status' , content => $Ticket->Status &>
+       </TD>
+       <TD VALIGN=TOP WIDTH="20%">
+        <& /Elements/ShadedBox, title => 'Worked' , content => $TimeWorked ." min" &>
+       </TD>
+       <TD VALIGN=TOP WIDTH="20%">
+         <& /Elements/ShadedBox, title => 'Priority', content=> $Ticket->Priority."/".$Ticket->FinalPriority &>
+       </TD>
+       <TD VALIGN=TOP WIDTH="20%">
+         <& /Elements/ShadedBox, title => 'Queue', content=> $Ticket->QueueObj->Name &>
+       </TD>   
+
+
+      </TR>
+    </TABLE>
+<%INIT>
+my $TimeWorked = $Ticket->TimeWorked;
+if ($Ticket->TimeLeft > 0 ) {
+        $TimeWorked = $Ticket->TimeWorked."/".$Ticket->TimeLeft;
+}
+</%INIT>
+<%ARGS>
+$Ticket => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ShowDates b/rt/webrt/Ticket/Elements/ShowDates
new file mode 100755 (executable)
index 0000000..e17e313
--- /dev/null
@@ -0,0 +1,54 @@
+<TABLE>
+<TR>
+<TD>
+Created:
+</TD>
+<TD>
+<% $Ticket->CreatedObj->AsString %>
+</TD>
+</TR>
+<TR>
+<TD>
+Starts: 
+</TD>
+<TD>
+<% $Ticket->StartsObj->AsString %> <BR>
+</TD>
+</TR>
+<TR>
+<TD>
+Started:
+</TD>
+<TD>
+<% $Ticket->StartedObj->AsString %>
+</TD>
+</TR>
+
+<TR>
+<TD>
+<a href="Display.html?id=<%$Ticket->id%>&Action=SetTold">Last Contact</a>:
+</TD>
+<TD>
+<% $Ticket->ToldObj->AsString %>
+</TD>
+</TR>
+<TR>
+<TD>
+Due:
+</TD>
+<TD><% $Ticket->DueObj->AsString  %>
+</TD>
+</TR>
+<TR>
+<TD>
+Updated:
+</TD>
+<TD>
+<A HREF="#lasttrans">
+<% $Ticket->LastUpdated ? ($Ticket->LastUpdatedAsString ." by ".$Ticket->LastUpdatedByObj->Name) : "Never" | h %></a>
+</TD>
+</TR>
+</TABLE>
+<%ARGS>
+$Ticket => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ShowDependencies b/rt/webrt/Ticket/Elements/ShowDependencies
new file mode 100755 (executable)
index 0000000..488652f
--- /dev/null
@@ -0,0 +1,18 @@
+Depends on:<BR>
+% while (my $Link = $Ticket->DependsOn->Next) {
+% my $member = $Link->TargetObj;
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%>
+[<%$member->Status%>]
+ <br>
+% }
+Depended on by:<BR>
+% while (my $Link = $Ticket->DependedOnBy->Next) {
+% my $member = $Link->TargetObj;
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%> 
+[<%$member->Status%>]
+ <br>
+% }
+
+<%ARGS>
+$Ticket => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ShowHistory b/rt/webrt/Ticket/Elements/ShowHistory
new file mode 100755 (executable)
index 0000000..155eaaa
--- /dev/null
@@ -0,0 +1,43 @@
+<TABLE BORDER=0 width="100%">
+<TR>
+<TD ALIGN=LEFT>
+% if ($ShowTitle) {
+<font size=+3>History</font>
+% }
+&nbsp;</TD>
+<TD align=right><font size=-1>Display mode: 
+%  if ($ShowHeaders == $Ticket->Id) {
+[<A HREF="<%$URIFile%>?id=<%$Ticket->id%>">Brief headers</a>]
+<b>[Full headers]</b>
+% } else {
+<b>[Brief headers]</b>
+[<A HREF="<%$URIFile%>?ShowHeaders=<%$Ticket->Id%>&id=<%$Ticket->id%>">Full headers</a>]
+%  }
+</font>
+</TD>
+</TR>
+</TABLE>
+
+<TABLE WIDTH=100% CELLSPACING=0 CELLPADDING=2 BORDER=0>
+% while (my $Transaction = $Transactions->Next) {
+% $i++;
+%      if ($Transactions->IsLast) {
+       <a name="lasttrans"></a>
+%      }
+           <& ShowTransaction, Ticket => $Ticket, Transaction => $Transaction, ShowHeaders => $ShowHeaders, Collapsed => $Collapsed, RowNum => $i  &>
+% }
+</TABLE>
+<%INIT>
+
+my $Transactions = $Ticket->Transactions;
+my $i;
+
+
+</%INIT>
+<%ARGS>
+$URIFile => 'Display.html'
+$Ticket => undef
+$ShowHeaders => undef
+$Collapsed => undef
+$ShowTitle => 1
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ShowKeywordSelects b/rt/webrt/Ticket/Elements/ShowKeywordSelects
new file mode 100644 (file)
index 0000000..4f8a178
--- /dev/null
@@ -0,0 +1,26 @@
+<TABLE>
+% while ( my $KeywordSelect = $KeywordSelects->Next ) {
+    <TR>
+      <TD VALIGN=TOP>
+       <% $KeywordSelect->Name %><BR>
+      </TD>
+      <TD VALIGN=TOP>
+       <UL>
+% my $Keywords = $Ticket->KeywordsObj($KeywordSelect->Id);
+% while (my $Keyword = $Keywords->Next) { 
+       <li><% $Keyword->KeywordObj->RelativePath($KeywordSelect->KeywordObj) |n %></li>
+
+%   }
+       </ul>
+      </TD>
+    </TR>
+% }
+</TABLE>
+
+<%INIT>
+my $KeywordSelects = $Ticket->QueueObj->KeywordSelects;
+</%INIT>
+
+<%ARGS>
+$Ticket => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ShowLinks b/rt/webrt/Ticket/Elements/ShowLinks
new file mode 100755 (executable)
index 0000000..4979595
--- /dev/null
@@ -0,0 +1,61 @@
+Depends on:<BR>
+<UL>
+% while (my $Link = $Ticket->DependsOn->Next) {
+% my $member = $Link->TargetObj;
+<LI><a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%>
+[<%$member->Status%>]
+
+% }
+</UL>
+
+Depended on by:<BR>
+<UL>
+% while (my $Link = $Ticket->DependedOnBy->Next) {
+% my $member = $Link->BaseObj;
+<LI><a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%> 
+[<%$member->Status%>]
+% }
+</UL>
+Parents:<BR>   
+<UL>
+% while (my $Link = $Ticket->MemberOf->Next) {
+% my $member = $Link->TargetObj;
+<LI><a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%>
+[<%$member->Status%>]
+
+% }
+</UL>
+
+Children:<BR>
+<& /Ticket/Elements/ShowMembers, Ticket => $Ticket &>
+<BR>
+Refers to:<BR>
+<UL>
+% while (my $Link = $Ticket->RefersTo->Next) {
+<LI>
+% if ($Link->TargetIsLocal) {
+% my $member = $Link->TargetObj;
+
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%> [<%$member->Status%>]<br>
+% } else {
+<A HREF="<%$Link->TargetAsHREF%>"><%$Link->Target%></A>
+% }
+%}
+</UL>
+
+Referred to by:<BR>
+<UL>
+% while (my $Link = $Ticket->ReferredToBy->Next) {
+<LI>
+% if ($Link->BaseIsLocal) {
+% my $member = $Link->BaseObj;
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%> [<%$member->Status%>]<br>
+% } else {
+<A HREF="<%$Link->BaseAsHREF%>"><%$Link->Base%></A>
+%}
+% }
+</UL>
+
+<%ARGS>
+$Ticket => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ShowMemberOf b/rt/webrt/Ticket/Elements/ShowMemberOf
new file mode 100755 (executable)
index 0000000..df5dc92
--- /dev/null
@@ -0,0 +1,12 @@
+<UL>
+% my $memberof = $Ticket->MemberOf;
+% while (my $member_of = $memberof->Next) {
+<LI><a href="/Ticket/Display.html?id=<%$member_of->Id%>"><%$member_of->Id%></a>: <%$member_of->Subject%> [<%$member_of->Status%>]
+% }
+</UL>
+
+<%INIT>
+</%INIT>
+<%ARGS>
+$Ticket => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ShowMembers b/rt/webrt/Ticket/Elements/ShowMembers
new file mode 100755 (executable)
index 0000000..0a6f123
--- /dev/null
@@ -0,0 +1,22 @@
+% if ($members->Count) {
+<UL>
+% while (my $link = $members->Next) {
+% my $member= $link->BaseObj;
+<LI><a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: <%$member->Subject%> [<%$member->Status%>]<br>
+% if ($depth < 8) {
+<&/Ticket/Elements/ShowMembers, Ticket => $member, depth => ($depth+1) &> 
+% }
+% }
+</UL>
+% }
+
+<%INIT>
+
+my $members = $Ticket->Members;
+
+</%INIT>
+
+<%ARGS>
+$Ticket => undef
+$depth => 1
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ShowPeople b/rt/webrt/Ticket/Elements/ShowPeople
new file mode 100755 (executable)
index 0000000..ff35f48
--- /dev/null
@@ -0,0 +1,12 @@
+Owner<BR>
+&nbsp;<B><%$Ticket->OwnerObj->Name%></B><BR>
+Requestors<BR>
+&nbsp;<B><%$Ticket->RequestorsAsString%></B><BR>
+Cc<BR>
+&nbsp;<B><%$Ticket->CcAsString%></B><BR>
+AdminCc<BR>
+&nbsp;<B><%$Ticket->AdminCcAsString%></B>
+<%ARGS>
+$Ticket => undef
+</%ARGS>
+
diff --git a/rt/webrt/Ticket/Elements/ShowReferences b/rt/webrt/Ticket/Elements/ShowReferences
new file mode 100755 (executable)
index 0000000..37e2fde
--- /dev/null
@@ -0,0 +1,27 @@
+<UL>
+% while (my $Link = $Ticket->RefersTo->Next) {
+<LI>
+% if ($Link->TargetIsLocal) {
+% my $member = $Link->TargetObj;
+
+<a href="/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%> [<%$member->Status%>]<br>
+% } else {
+<A HREF="<%$Link->TargetAsHREF%>"><%$Link->Target%></A>
+% }
+%}
+
+
+
+% while (my $Link = $Ticket->ReferredToBy->Next) {
+<LI>
+% if ($Link->BaseIsLocal) {
+% my $member = $Link->BaseObj;
+<a href="/Ticket/Display.html?id=<%$member->Id%>"><%$member->Id%></a>: (<%$member->OwnerObj->Name%>) <%$member->Subject%> [<%$member->Status%>]<br>
+% } else {
+<A HREF="<%$Link->BaseAsHREF%>"><%$Link->Base%></A>
+%}
+% }
+</UL>
+<%ARGS>
+$Ticket => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ShowRequestor b/rt/webrt/Ticket/Elements/ShowRequestor
new file mode 100644 (file)
index 0000000..fcbe71d
--- /dev/null
@@ -0,0 +1,35 @@
+<%PERL>
+my $people = $Ticket->Requestors;
+while (my $requestor=$people->Next) {
+if (($requestor->Owner ) && (my $user=$requestor->OwnerObj)) {
+my $name=$user->RealName || $user->EmailAddress;       
+my $tickets = new RT::Tickets($session{'CurrentUser'});
+$tickets->LimitRequestor(VALUE => $user->EmailAddress);
+$tickets->LimitStatus( VALUE => 'open');
+$tickets->LimitStatus( VALUE => 'new');
+$tickets->RowsPerPage(25);
+$tickets->OrderBy(FIELD => 'Priority',
+                 ORDER => 'DESC');
+</%PERL>
+
+% unless ($user->Privileged) {
+<& /Elements/TitleBoxStart, 
+       title => "<a class='inverse' href=\"$RT::WebPath/Admin/Users/Modify.html?id=".$user->id."\">More about $name</a>" &>
+
+Comments about this user:<BR>
+<B><% ($user->Comments || "No comment entered about this user") %></B><BR>
+
+This user's 25 highest priority tickets:<BR>
+<UL>
+%while (my $w=$tickets->Next) {
+<LI><%$w->Id%>: <a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$w->id%>"><%$w->Subject%></a> (<%$w->Status%>)
+%}
+</UL>
+<& /Elements/TitleBoxEnd &>
+
+% }
+% }
+%}
+<%ARGS>
+$Ticket=>undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ShowSummary b/rt/webrt/Ticket/Elements/ShowSummary
new file mode 100755 (executable)
index 0000000..b80ceb4
--- /dev/null
@@ -0,0 +1,61 @@
+      <TABLE WIDTH="100%" >
+      <TR>
+       <TD VALIGN=TOP >
+         <& /Elements/TitleBoxStart, title => 'The Basics', 
+               title_href =>"$RT::WebPath/Ticket/Modify.html?id=".$Ticket->Id, 
+               title_class=> 'inverse',  
+               color => "#993333" &>
+         <& /Ticket/Elements/ShowBasics, Ticket => $Ticket &>
+         <& /Elements/TitleBoxEnd &>
+
+       <BR>
+
+         <& /Elements/TitleBoxStart, 
+               title_href =>"$RT::WebPath/Ticket/Modify.html?id=".$Ticket->Id, 
+               title_class=> 'inverse',  
+               title => "Keyword Selections", color => "#993300"
+         &>
+         <& /Ticket/Elements/ShowKeywordSelects, Ticket => $Ticket &>
+         <& /Elements/TitleBoxEnd &>
+
+       
+
+       <BR>
+         <& /Elements/TitleBoxStart, title => 'Relationships', 
+               title_href => "$RT::WebPath/Ticket/ModifyLinks.html?id=".$Ticket->Id, 
+               title_class=> 'inverse',  
+               titleright => '', color=> "#336633" &>
+         <& /Ticket/Elements/ShowLinks, Ticket => $Ticket &>
+       <& /Elements/TitleBoxEnd &>
+       </TD>
+       <BR>
+       <TD VALIGN=TOP >
+
+         <& /Elements/TitleBoxStart, title => "Dates",
+               title_href =>"$RT::WebPath/Ticket/ModifyDates.html?id=".$Ticket->Id, 
+               title_class=> 'inverse',  
+                color => "#663366" &>
+         <& /Ticket/Elements/ShowDates, Ticket => $Ticket &>
+         <& /Elements/TitleBoxEnd &>
+       <BR>  
+         <& /Elements/TitleBoxStart, title => 'People', 
+               title_href =>"$RT::WebPath/Ticket/ModifyPeople.html?id=".$Ticket->Id, 
+               title_class=> 'inverse',  
+               color => "#333399" &>
+         <& /Ticket/Elements/ShowPeople, Ticket => $Ticket &>
+         <& /Elements/TitleBoxEnd &>
+       <BR>
+
+         <& /Ticket/Elements/ShowRequestor, Ticket => $Ticket &>
+
+
+       </TD>
+      </TR>
+    </TABLE>
+<%ARGS>
+$Ticket => undef
+</%ARGS>
+
+
+
+
diff --git a/rt/webrt/Ticket/Elements/ShowTransaction b/rt/webrt/Ticket/Elements/ShowTransaction
new file mode 100755 (executable)
index 0000000..a0da008
--- /dev/null
@@ -0,0 +1,162 @@
+<TR bgcolor="<%$rowbgcolor%>">
+<TD bgcolor="<%$bgcolor%>"><A NAME="#<%$Transaction->Id%>"></A>&nbsp&nbsp;</TD>
+<TD>&nbsp&nbsp;</TD>
+<TD><font size=-2><% $transdate|n %></font>&nbsp;</TD>
+<TD ALIGN="LEFT"><b><%$Transaction->CreatorObj->Name%> - <%$TicketString%> <%$Transaction->BriefDescription%>
+
+</b></TD>
+<TD><%$TimeTaken%>&nbsp;</TD>
+<TD ALIGN="RIGHT"><font size=-1><%$titlebar_commands|n%></font></TD>
+</TR>
+<%PERL>
+
+unless ($Collapsed) {
+ $attachments->GotoFirstItem;
+ while (my $message=$attachments->Next) {
+     #we don't want to show any empty transactions, unless they have kids
+     next unless (length $message->Content || $message->Children->Count);
+     my ($headers, $content);
+     
+    </%PERL>
+
+
+<%PERL>
+  if ($message->Parent == 0) {
+      if ($ShowHeaders == $Ticket->Id) {
+         $headers = $message->Headers;
+      } else {
+         $headers = $message->NiceHeaders;
+      }
+      chomp $headers;
+      $headers .= "\n\n" if ($headers);
+  }
+     # 13456 is a random # of about the biggest size we want to see inline text
+     my $MAX_INLINE_BODY = 13456;
+     if ($message->ContentType =~ m{^(text/plain|message|text$)}i && 
+                                   length($message->Content)< $MAX_INLINE_BODY ) {
+
+        $content = $message->Content;
+
+        my $wrapper = new Text::Wrapper (columns=>85);
+        $content = $wrapper->wrap($content);
+         $content =~ s/&/&amp;/g;
+        $content =~ s/</&lt;/g;
+        $content =~ s/>/&gt;/g;
+         $content =~ s!((?:http|https|ftp|mailto):\S*?)([\s"']|&gt;|\.[\n])!<A HREF=\"$1\" TARGET=new>$1</A>$2!g;
+
+
+     }
+     else {
+        $content = "&nbsp;";
+     }
+        
+</%PERL>
+<TR BGCOLOR="<%$rowbgcolor%>">
+      <TD BGCOLOR="<%$bgcolor%>">&nbsp;&nbsp;</TD>
+      <TD>&nbsp&nbsp;</TD>
+      <TD COLSPAN=3 VALIGN=TOP>
+       <PRE>
+<%$headers%><%$content|n%>
+</PRE>
+      </TD>
+      <TD VALIGN=TOP ALIGN=RIGHT>
+       
+% if ($message->Parent == 0  ) {
+<BR>
+% }
+<%PERL>
+my $size = length($message->Content());
+
+if ($size) {
+    if ($size > 1024) {
+       $size = int($size/102.4)/10 . "k";
+    }
+    else {
+       $size = $size ."b";
+    }
+</%PERL>
+<font size=-1><A HREF="Attachment/<%$Transaction->Id%>/<%$message->Id%>/<%$message->Filename%>">Download <%$message->Filename|| '(untitled)'%></a> <% $size %></font>
+% }
+</TD>
+</TR>
+% }
+% }
+
+
+
+<%ARGS>
+$Ticket => undef
+$Transaction => undef
+$ShowHeaders => undef
+$Collapsed => undef
+$ShowTitleBarCommands => 1
+$RowNum => 1
+</%ARGS>
+
+<%INIT>
+
+
+my ($TimeTaken, $TicketString, $bgcolor, $rowbgcolor);
+
+my $transdate = $Transaction->CreatedAsString();
+$transdate =~ s/\s/&nbsp;/g;
+
+if ($RowNum % 2) {
+       $rowbgcolor="#cccccc";
+} else {
+       $rowbgcolor="#ffffff";
+}
+
+if ($Transaction->Type =~ /^(Create|Correspond|Comment$)/) {
+       if ($Transaction->IsInbound) {
+               $bgcolor="#336699";
+       }
+       else {
+               $bgcolor="#339999";
+       }
+} elsif (($Transaction->Field =~ /^Owner$/) or 
+        ($Transaction->Type =~ /^(AddWatcher|DelWatcher)$/)) {
+       $bgcolor="#333399";
+
+} elsif ($Transaction->Type =~ /^(AddLink|DeleteLink)$/) {
+       $bgcolor="#336633";
+} elsif ($Transaction->Type =~ /^(Status|Set|Keyword|Told)$/) {
+       if ($Transaction->Field =~ /^(Told|Starts|Started|Due)$/) {
+               $bgcolor="#663366";     
+       }
+       else {
+               $bgcolor="#993333";
+       }
+}
+else {
+       $bgcolor="#cccccc";
+}
+
+if ($Ticket->Id != $Transaction->Ticket) {
+       $TicketString = "Ticket ".$Transaction->Ticket .": ";
+}
+
+if ($Transaction->TimeTaken > 0) {
+       $TimeTaken = $Transaction->TimeTaken." min"
+}
+my $attachments = $Transaction->Attachments;
+
+my $titlebar_commands='&nbsp;';
+
+# If the transaction has anything attached to it at all
+if ($Transaction->Message->First && $ShowTitleBarCommands) {
+       if ($Transaction->TicketObj->CurrentUserHasRight('ReplyToTicket')) {
+               $titlebar_commands .= 
+                 "[<a href=\"Update.html?id=".
+                 $Transaction->Ticket . "&QuoteTransaction=".$Transaction->Id.
+                 "&Action=Respond\">Reply</a>]&nbsp;";
+       }
+       if ($Transaction->TicketObj->CurrentUserHasRight('CommentOnTicket')) {
+            $titlebar_commands .= 
+            "[<a href=\"Update.html?id=".$Transaction->Ticket. 
+            "&QuoteTransaction=".$Transaction->Id.
+            "&Action=Comment\">Comment</a>]";
+       }
+}
+
+</%INIT>
diff --git a/rt/webrt/Ticket/Elements/Tabs b/rt/webrt/Ticket/Elements/Tabs
new file mode 100755 (executable)
index 0000000..8cce197
--- /dev/null
@@ -0,0 +1,126 @@
+<& /Elements/Tabs, tabs => $tabs, actions => $actions, current_tab => $current_tab, tabs_scalar => $tabs_scalar &>
+<%INIT>
+       
+  my $id = $Ticket->id();
+  my $tabs_scalar = '';
+  my $tabs = {
+                A => { title => 'Display',
+                       path => "Ticket/Display.html?id=".$id,
+                     },
+             
+             Ab => { title => 'History',
+                     path => "Ticket/History.html?id=".$id,
+                      },
+             B => { title => 'Basics',
+                    path => "Ticket/Modify.html?id=".$id,
+                  },
+             
+             C => { title => 'Dates',
+                    path => "Ticket/ModifyDates.html?id=".$id,
+                  },
+             
+             D => { title => 'People',
+                    path => "Ticket/ModifyPeople.html?id=".$id,
+                  },
+             E => { title => 'Links',
+                    path => "Ticket/ModifyLinks.html?id=".$id,
+                  },
+             F => { title => 'Jumbo',
+                    path => "Ticket/ModifyAll.html?id=".$id,
+                  },
+             
+            };
+
+my $actions;
+if ($Ticket->CurrentUserHasRight('ModifyTicket') or 
+    $Ticket->CurrentUserHasRight('CommentOnTicket')) {
+    $actions->{'Comment'} = 
+      { 
+       title => 'Comment',
+       path => "Ticket/Update.html?Action=Comment&id=".$id,
+      }
+  };
+
+if ($Ticket->CurrentUserHasRight('ModifyTicket') or 
+    $Ticket->CurrentUserHasRight('ReplyToTicket')) {
+    $actions->{'Reply'} = 
+      { 
+       title => 'Reply',
+       path => "Ticket/Update.html?Action=Respond&id=".$id,
+      }
+  };
+
+if ($Ticket->CurrentUserHasRight('OwnTicket')) {
+    if ($Ticket->OwnerObj->id == $RT::Nobody->id)  {
+       $actions->{'Take'} =    
+         { 
+          path => "Ticket/Display.html?Action=Take&id=".$id,
+          title => 'Take'
+         };
+    }
+    elsif ( $Ticket->OwnerObj->id != $session{CurrentUser}->id) {
+       $actions->{'Steal'} =   
+         { 
+          path => "Ticket/Display.html?Action=Steal&id=".$id,
+          title => 'Steal' 
+         };
+    }
+}
+
+if ($Ticket->CurrentUserHasRight('ModifyTicket')) {
+    if ($Ticket->Status ne 'resolved') {
+       $actions->{'Resolve'} = 
+         { 
+
+           path => "Ticket/Update.html?Action=Comment&DefaultStatus=resolved&id=".$id,
+          title => 'Resolve'
+         };
+    }  
+    if ($Ticket->Status ne 'open') {   
+       $actions->{'Open'} = 
+         { 
+          path => "Ticket/Display.html?Status=open&id=". $id,
+          title => 'Open'
+         };
+    }
+}
+
+
+
+
+if (defined $session{'tickets'}) {
+    my $items = $session{'tickets'}->ItemsArrayRef();
+    my @indexs = grep(($items->[$_]->id == $Ticket->Id), 0 .. $#{$items});
+
+    if ($items->[0]) {
+
+    if ($items->[$indexs[0]]->id == $Ticket->Id) {
+       # Don't display prev links if we're on the first ticket
+       if ( $items->[0]->id != $Ticket->id ) {
+           $tabs_scalar .= '[<A HREF="Display.html?id='.
+             $items->[0]->id.
+               '">&lt;&lt; First</a>] ';
+           $tabs_scalar .= '[<A HREF="Display.html?id='.
+             $items->[$indexs[0]-1]->id.
+               '">&lt; Prev</a>] ';
+       }
+       # Don't display next links if we're on the last ticket
+       if ( $Ticket->id != $items->[-1]->id ) {
+           $tabs_scalar .= '[<A HREF="Display.html?id='.
+             $items->[$indexs[0]+1]->id.
+               '">Next &gt;</a>] ';
+           $tabs_scalar .= '[<A HREF="Display.html?id='.
+             $items->[-1]->id.
+               '">Last &gt;&gt</a>]';
+       }
+       $tabs_scalar .= "<BR><BR>";
+    }
+    }
+}
+</%INIT>
+
+  
+<%ARGS>
+$Ticket => undef
+$current_tab => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/Elements/ToolBar b/rt/webrt/Ticket/Elements/ToolBar
new file mode 100755 (executable)
index 0000000..108e2f7
--- /dev/null
@@ -0,0 +1,3 @@
+<%ARGS>
+$Ticket => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/History.html b/rt/webrt/Ticket/History.html
new file mode 100755 (executable)
index 0000000..e0a5fe1
--- /dev/null
@@ -0,0 +1,30 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Ticket/Attic/History.html,v 1.1 2002-08-12 06:17:09 ivan Exp $
+%# Copyright 1996-2000 Jesse Vincent <jesse@fsck.com>
+
+<& /Elements/Header, Title => "Ticket History #".$Ticket->Id ." ".$Ticket->Subject &>
+<& /Ticket/Elements/Tabs, Ticket => $Ticket, current_tab => 'Ticket/History.html?id='.$Ticket->id  &>
+
+<BR>
+      
+<& /Ticket/Elements/ShowHistory , Ticket => $Ticket, ShowHeaders => $ARGS{'ShowHeaders'}, URIFile => 'History.html' &> 
+
+
+<%ARGS>
+$id => undef
+</%ARGS>
+
+<%INIT>
+
+  
+
+my $Ticket = LoadTicket ($id);
+
+unless ($Ticket->CurrentUserHasRight('ShowTicket')) {
+       Abort("No permission to view ticket");
+}
+
+</%INIT>
+
+
+
+
diff --git a/rt/webrt/Ticket/Modify.html b/rt/webrt/Ticket/Modify.html
new file mode 100755 (executable)
index 0000000..7a8a792
--- /dev/null
@@ -0,0 +1,39 @@
+<& /Elements/Header, Title => 'Modify ticket #'.$TicketObj->Id &>
+<& /Ticket/Elements/Tabs, Ticket => $TicketObj, current_tab => "Ticket/Modify.html?id=".$TicketObj->Id  &>
+
+<& /Elements/ListActions, actions => \@results &>
+<FORM METHOD=POST ACTION="Modify.html">
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$TicketObj->Id%>">
+
+<& /Elements/TitleBoxStart, title => 'Modify ticket #'.$TicketObj->Id, 
+  color=> "#993333", width => "100%" &>
+<& Elements/EditBasics, TicketObj => $TicketObj &>
+<& /Elements/TitleBoxEnd &>
+
+<& /Elements/TitleBoxStart, title => 'Keywords', color =>"#993333"&>
+<& Elements/EditKeywordSelects, TicketObj=>$TicketObj &>
+<& /Elements/TitleBoxEnd &>
+
+<& /Elements/Submit, Label => 'Save Changes', Caption => "If you've updated anything above, be sure to", color => "#993333" &>
+</form>
+<%INIT>
+  
+my $TicketObj = LoadTicket($id);
+
+my @results = ProcessTicketBasics(TicketObj => $TicketObj, ARGSRef => \%ARGS);
+my @okresults = ProcessTicketObjectKeywords(TicketObj => $TicketObj, ARGSRef => \%ARGS);
+
+push (@results, @okresults);
+
+# TODO: display the results, even if we can't display the ticket
+
+unless ($TicketObj->CurrentUserHasRight('ShowTicket')) {
+     Abort("No permission to view ticket");
+} 
+
+</%INIT>
+
+
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/ModifyAll.html b/rt/webrt/Ticket/ModifyAll.html
new file mode 100755 (executable)
index 0000000..ad91373
--- /dev/null
@@ -0,0 +1,124 @@
+<& /Elements/Header, Title => "Ticket #".$Ticket->Id ." Jumbo update: ".$Ticket->Subject &>
+<& /Ticket/Elements/Tabs, Ticket => $Ticket , current_tab => "Ticket/ModifyAll.html?id=".$Ticket->Id &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<FORM METHOD=POST ACTION="ModifyAll.html" ENCTYPE="multipart/form-data">
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$Ticket->Id%>">
+
+
+<& /Elements/TitleBoxStart, title => 'Modify ticket #'.$Ticket->Id,   color=> "#993333", width => "100%" &>
+<& Elements/EditBasics, TicketObj => $Ticket &>
+<& /Elements/TitleBoxEnd &>
+
+<BR>
+
+<& /Elements/TitleBoxStart, title => 'Dates',  width => "100%", color => "#663366"  &>
+<& Elements/EditDates, TicketObj => $Ticket &>
+<& /Elements/TitleBoxEnd &>
+
+<BR>
+
+<& /Elements/TitleBoxStart, title => 'Keywords', color =>"#993333"&>
+<& Elements/EditKeywordSelects, TicketObj=>$Ticket &>
+<& /Elements/TitleBoxEnd &>
+
+<BR>
+
+<& /Elements/TitleBoxStart, title => 'People',width => "100%", color=> "#333399" &>
+<& Elements/EditPeople, Ticket => $Ticket, UserField => $UserField, UserString => $UserString, UserOp => $UserOp &>
+<& /Elements/TitleBoxEnd &>
+
+<BR>
+
+<& /Elements/TitleBoxStart, title => 'Relationships', color => "#336633"&>
+<& Elements/EditLinks, Ticket => $Ticket &>
+<& /Elements/TitleBoxEnd &>
+
+<BR>
+
+<& /Elements/TitleBoxStart, title => 'Update ticket' &>
+<hr>
+Update Type: <select name="UpdateType">
+% if ($CanComment) {
+  <option value="private" >Comments (Not sent to requestors)</option>
+% }
+% if ($CanRespond) {
+   <option value="response">Response to requestors</option>
+% }
+</select> 
+<br>
+
+Subject: <input name="UpdateSubject" size=60 value=""> <br>
+Attach: <input name="UpdateAttachment" type=file> <br>
+<& /Elements/MessageBox, Name=>"UpdateContent", QuoteTransaction=>$ARGS{QuoteTransaction} &>
+<& /Elements/TitleBoxEnd &>
+
+
+<& /Elements/Submit, Label => 'Save Changes', Caption => "If you've updated anything above, be sure to", color => "#333399" &>
+</form>
+
+<%INIT>
+
+
+
+my $Ticket = LoadTicket($id);
+
+my $CanRespond = 0;
+my $CanComment = 0;
+
+
+$CanRespond = 1 if ( $Ticket->CurrentUserHasRight('ReplyToTicket') or
+                     $Ticket->CurrentUserHasRight('ModifyTicket') ); 
+
+$CanComment = 1 if ( $Ticket->CurrentUserHasRight('CommentOnTicket') or
+                     $Ticket->CurrentUserHasRight('ModifyTicket') );
+
+
+my (@wresults, @results, @okresults, @dresults, @lresults);
+
+unless ($OnlySearchForPeople) {
+    @wresults = ProcessTicketWatchers( TicketObj => $Ticket, ARGSRef => \%ARGS);
+    @results = ProcessTicketBasics( TicketObj => $Ticket, ARGSRef => \%ARGS);
+    @okresults = ProcessTicketObjectKeywords(TicketObj => $Ticket, ARGSRef => \%ARGS);
+
+    @dresults = ProcessTicketDates( TicketObj => $Ticket, ARGSRef => \%ARGS);
+    @lresults = ProcessTicketLinks( TicketObj => $Ticket, ARGSRef => \%ARGS);
+
+    $ARGS{'UpdateContent'} =~ s/\r\n/\n/g;
+
+    if ($ARGS{'UpdateContent'} && 
+       $ARGS{'UpdateContent'} ne '' && 
+       $ARGS{'UpdateContent'} ne  "-- \n" . 
+                               $session{'CurrentUser'}->UserObj->Signature
+       ) {
+        ProcessUpdateMessage(TicketObj => $Ticket, 
+                             ARGSRef=>\%ARGS, 
+                              Actions=>\@results);
+       }
+}
+push @results, @wresults;
+push @results, @dresults;
+push @results, @lresults;
+push @results, @okresults;
+
+# If they've gone and moved the ticket to somewhere they can't see, etc...
+# TODO: display the results, even if we can't display the ticket.
+
+unless ($Ticket->CurrentUserHasRight('ShowTicket')) {
+   Abort("No permission to view ticket");
+}
+
+
+</%INIT>
+
+
+
+<%ARGS>
+$OnlySearchForPeople => undef
+$UserField => undef
+$UserOp => undef
+$UserString => undef
+$id => undef
+</%ARGS>
+
diff --git a/rt/webrt/Ticket/ModifyDates.html b/rt/webrt/Ticket/ModifyDates.html
new file mode 100755 (executable)
index 0000000..b2ecb68
--- /dev/null
@@ -0,0 +1,26 @@
+<& /Elements/Header, Title => 'Modify dates for #'. $TicketObj->Id &>
+<& /Ticket/Elements/Tabs, Ticket => $TicketObj, current_tab => "Ticket/ModifyDates.html?id=".$TicketObj->Id  &> 
+
+<& /Elements/ListActions, actions => \@results &>
+
+<FORM METHOD=POST ACTION="ModifyDates.html">
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$TicketObj->Id%>">
+<& /Elements/TitleBoxStart, title => 'Modify dates for ticket #'.$TicketObj->Id,  width => "100%", color => "#663366"  &>
+
+<& Elements/EditDates, TicketObj => $TicketObj &>
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit, color => "#663366" &>
+</form>
+
+
+<%INIT>
+
+my $TicketObj = LoadTicket($id);
+my @results = ProcessTicketDates( TicketObj => $TicketObj, ARGSRef => \%ARGS);
+
+</%INIT>
+
+
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/ModifyLinks.html b/rt/webrt/Ticket/ModifyLinks.html
new file mode 100755 (executable)
index 0000000..14c939d
--- /dev/null
@@ -0,0 +1,31 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Ticket/Attic/ModifyLinks.html,v 1.1 2002-08-12 06:17:09 ivan Exp $
+%# Copyright 1996-2000 Jesse Vincent <jesse@fsck.com>
+
+<& /Elements/Header, Title => "Link ticket ".$Ticket->Id &>
+<& /Ticket/Elements/Tabs, Ticket => $Ticket, current_tab => "Ticket/ModifyLinks.html?id=".$Ticket->Id &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<form action="ModifyLinks.html" method="post">
+<input type="hidden" name="id" value="<%$Ticket->id%>">
+
+<& /Elements/TitleBoxStart, title => 'Edit Relationships', color => "#336633"&>
+<& Elements/EditLinks, Ticket => $Ticket &>
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit, color => "#336633", Caption=> 'Save changes' &>
+</form>
+
+
+
+
+<%INIT>
+  
+my $Ticket = LoadTicket($id);
+my @results = ProcessTicketLinks( TicketObj => $Ticket, ARGSRef => \%ARGS);
+    
+</%INIT>
+      
+      
+<%ARGS>
+$id => undef
+</%ARGS>
diff --git a/rt/webrt/Ticket/ModifyPeople.html b/rt/webrt/Ticket/ModifyPeople.html
new file mode 100755 (executable)
index 0000000..fecf091
--- /dev/null
@@ -0,0 +1,38 @@
+<& /Elements/Header, Title => 'Modify people related to ticket # ' . $Ticket->id &>
+<& /Ticket/Elements/Tabs, Ticket => $Ticket , current_tab => "Ticket/ModifyPeople.html?id=".$Ticket->Id &>
+
+<& /Elements/ListActions, actions => \@results &>
+
+<FORM METHOD=POST ACTION="ModifyPeople.html">
+<INPUT TYPE=HIDDEN NAME=id VALUE="<%$Ticket->Id%>">
+<& /Elements/TitleBoxStart, title => 'Modify people related to ticket #'.$Ticket->Id,   width => "100%", color=> "#333399" &>
+<& Elements/EditPeople, Ticket => $Ticket, UserField => $UserField, UserString => $UserString, UserOp => $UserOp &>
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit, Label => 'Save Changes', Caption => "If you've updated anything above, be sure to", color => "#333399" &>
+</form>
+
+<%INIT>
+
+my (@results, @wresults);
+
+my $Ticket = LoadTicket($id);
+
+# if we're trying to search for watchers and nothing else
+unless ($OnlySearchForPeople) {
+    @results = ProcessTicketBasics( TicketObj => $Ticket, ARGSRef => \%ARGS);
+    @wresults = ProcessTicketWatchers( TicketObj => $Ticket, ARGSRef => \%ARGS);
+}
+
+push @results, @wresults;
+</%INIT>
+
+
+
+<%ARGS>
+$OnlySearchForPeople => undef
+$UserField => undef
+$UserOp => undef
+$UserString => undef
+$id => undef
+</%ARGS>
+
diff --git a/rt/webrt/Ticket/Update.html b/rt/webrt/Ticket/Update.html
new file mode 100755 (executable)
index 0000000..be22666
--- /dev/null
@@ -0,0 +1,110 @@
+<& /Elements/Header, Title=> $title  &>
+<& /Ticket/Elements/Tabs, Ticket => $Ticket &>
+<& /Elements/TitleBoxStart, title => "Update ticket" &>
+
+<FORM ACTION="Display.html" NAME="TicketUpdate" 
+       METHOD=POST enctype="multipart/form-data">
+
+<TABLE>
+<TR><TD>
+<a href="ModifyPeople.html?id=<%$Ticket->Id%>">Ticket watchers</A></TD><TD align=right>
+Requestor:
+</TD><TD>
+<b><% $Ticket->RequestorsAsString %></b>
+</TD></TR>
+<TR><TD>&nbsp;</TD><TD align=right>
+Cc:
+</TD><TD>
+<b><% $Ticket->CcAsString %></b>
+</TD></TR>
+<TR><TD>&nbsp;</TD><TD align=right>
+AdminCc:
+</TD><TD>
+<b><% $Ticket->AdminCcAsString %></b>
+</TD></TR>
+</TR>
+</TABLE>
+<hr>
+
+<TABLE BORDER=0>
+
+<tr><td align=right>Status:</td>
+<td>
+<& /Elements/SelectStatus, Name=>"Status", Default => $DefaultStatus &>
+Owner:  
+<& /Elements/SelectOwner, Name=>"Owner", Default => $Ticket->OwnerObj->Id(), QueueObj => $Ticket->QueueObj, TicketObj => $Ticket &>
+Worked: <input size=4 name="UpdateTimeWorked"> minutes</td></tr>
+<tr><td align=right>Update Type:</td>
+<td><select name="UpdateType">
+% if ($CanComment) {
+  <option value="private" <%$CommentDefault%>>Comments (Not sent to requestors)</option>
+% }
+% if ($CanRespond) {
+   <option value="response" <%$ResponseDefault%>>Response to requestors</option>
+% }
+</select> 
+</td></tr>
+<tr><td align=right>Subject:</td><td> <input name="UpdateSubject" size=60 value="<%$Ticket->Subject()%>"></td></tr>
+<tr><td align=right>Cc:</td><td> <input name="UpdateCc" size=60><BR>
+<i><font size=-2>(Sends a carbon-copy of this update to a comma-delimited list
+of email addresses. Does <b>not</b> change who will receive future updates.)</font></i>
+</td></tr>
+<tr><td align=right>Bcc:</td><td> <input name="UpdateBcc" size=60><BR>
+<i><font size=-2>(Sends a blind carbon-copy of this update to a comma-delimited list
+of email addresses. Does <b>not</b> change who will receive future updates.)</font></i>
+</td></tr>
+<tr><td align=right>Attach:</td><td><input name="UpdateAttachment" type="file"></td></tr>
+</table>
+<& /Elements/MessageBox, Name=>"UpdateContent", QuoteTransaction=>$ARGS{QuoteTransaction} &>
+               <INPUT TYPE=HIDDEN NAME=id VALUE="<%$Ticket->Id%>"><br>
+
+
+
+
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit &>
+  </FORM>
+
+
+
+<%INIT>
+
+my $CanRespond = 0;
+my $CanComment = 0;
+my $title;
+
+my $Ticket = LoadTicket($id);
+
+
+if ($DefaultStatus eq 'resolved') {
+       $title = "Resolve";
+} else {
+       $title = "Update";
+}
+
+$title .= " ticket #" . $Ticket->id . " (" .$Ticket->Subject.")";
+
+# Things needed in the template - we'll do the processing here, just
+# for the convinience:
+my $CommentDefault=$Action eq "Comment" ? "SELECTED" : "";
+my $ResponseDefault=$Action eq "Respond" ? "SELECTED" : "";
+
+$DefaultStatus = $Ticket->Status() unless ($DefaultStatus);
+
+$CanRespond = 1 if ( $Ticket->CurrentUserHasRight('ReplyToTicket') or
+                     $Ticket->CurrentUserHasRight('ModifyTicket') ); 
+
+$CanComment = 1 if ( $Ticket->CurrentUserHasRight('CommentOnTicket') or
+                     $Ticket->CurrentUserHasRight('ModifyTicket') ); 
+       
+
+     
+     
+
+</%INIT>
+
+<%ARGS>
+$id => undef
+$Action => undef
+$DefaultStatus => undef
+</%ARGS>
diff --git a/rt/webrt/User/Prefs.html b/rt/webrt/User/Prefs.html
new file mode 100755 (executable)
index 0000000..d769977
--- /dev/null
@@ -0,0 +1,53 @@
+<& /Elements/Header, Title=>"Preferences" &>
+<& /Elements/Tabs &>
+
+<& /Elements/ListActions, actions => \@results &>
+<form method=post>
+
+% unless ($RT::WebExternalAuth) {
+<& /Elements/TitleBoxStart, title => 'Change password'  &>
+New password: <input type=password name="NewPass1" size=16>
+Confirm: <input type=password name="NewPass2" size=16>
+<& /Elements/TitleBoxEnd &>
+<BR>
+% }
+<& /Elements/TitleBoxStart, title => 'Signature'  &>
+<INPUT TYPE=HIDDEN NAME="SignatureMagic" VALUE=1>
+<TEXTAREA COLS=72 ROWS=4 WRAP=HARD NAME="Signature"><% $session{'CurrentUser'}->UserObj->Signature %></TEXTAREA>
+<br>
+<BR>
+<& /Elements/TitleBoxEnd &>
+<& /Elements/Submit &>
+         </form>
+
+
+<%INIT>
+my @results;
+
+if ($NewPass1) {
+    if ($NewPass1 ne $NewPass2) {
+       push (@results, "Passwords did not match.");
+    }  
+    else {
+       my ($val, $msg)=$session{'CurrentUser'}->UserObj->SetPassword($NewPass1);
+       push (@results, "Password: ".$msg);
+    }  
+}
+if ($Signature || $SignatureMagic) {
+    $Signature =~ s/(\r\n|\r)/\n/g;
+    if ($Signature ne $session{'CurrentUser'}->UserObj->Signature) {
+       my ($val, $msg)=$session{'CurrentUser'}->UserObj->SetSignature($Signature);
+       push (@results, "Signature: ".$msg);
+    }
+}
+#A hack to make sure that session gets rewritten.
+
+$session{'i'}++;
+</%INIT>
+
+<%ARGS>
+$Signature => undef
+$SignatureMagic => undef
+$NewPass1 => undef
+$NewPass2 => undef
+</%ARGS>
diff --git a/rt/webrt/autohandler b/rt/webrt/autohandler
new file mode 100755 (executable)
index 0000000..16cdbc7
--- /dev/null
@@ -0,0 +1,73 @@
+%# $Header: /home/cvs/cvsroot/freeside/rt/webrt/Attic/autohandler,v 1.1 2002-08-12 06:17:08 ivan Exp $
+<& /Elements/Footer, %ARGS &>
+
+<%INIT>
+
+$m->{'rt_base_time'} = time;
+
+#if it's a noauth file, don't ask for auth.
+if ($m->base_comp->path =~ '^/+NoAuth/') {
+        $m->call_next();
+       $m->abort();
+}
+
+# If RT is configured for external auth, let's get REMOTE_USER
+# We intentionally don't test for REMOTE_USER to meet our policy
+elsif ($RT::WebExternalAuth){
+
+    $user = $ENV{'REMOTE_USER'};
+    $session{'CurrentUser'} = RT::CurrentUser->new();
+    $session{'CurrentUser'}->Load($user);
+    unless ($session{'CurrentUser'}->id() ) {
+        delete $session{'CurrentUser'};
+        $m->comp('/Elements/Login', %ARGS, Error=> 'You are not an authorized user');
+        $m->abort();
+    }
+}
+# If the user is loging in, let's authenticate
+elsif (defined ($user) && defined ($pass)){
+    
+    $session{'CurrentUser'} = RT::CurrentUser->new();
+    $session{'CurrentUser'}->Load($user);
+    unless ($session{'CurrentUser'}->id() ) {
+       delete $session{'CurrentUser'};
+       $m->comp('/Elements/Login', %ARGS, Error=> 'Your username or password is incorrect');
+        $m->abort();
+    };
+    unless ($session{'CurrentUser'}->IsPassword($pass)) {
+       delete $session{'CurrentUser'};
+       
+       $m->comp('/Elements/Login', Error => 'Your username or password is incorrect', %ARGS);
+       $m->abort();
+    }
+}
+  
+
+#If we've got credentials, lets serve the file up.
+if ( (defined $session{'CurrentUser'}) and 
+     ( $session{'CurrentUser'}->Id) ) {
+    
+    # If the user isn\'t privileged, they can only see SelfService
+    if ((! $session{'CurrentUser'}->Privileged) and
+       ($m->base_comp->path !~ '^/+SelfService/') ) {
+       $m->comp('/SelfService/index.html');
+       $m->abort();
+    }
+    else {
+       $m->call_next;
+    }
+}
+
+#If we have no credentials
+else {
+    $m->comp('/Elements/Login', %ARGS);
+    $m->abort();
+}
+
+</%INIT>
+
+<%ARGS>
+$user => undef
+$pass => undef
+</%ARGS>
diff --git a/rt/webrt/index.html b/rt/webrt/index.html
new file mode 100644 (file)
index 0000000..0c1091a
--- /dev/null
@@ -0,0 +1,25 @@
+<& /Elements/Header, Title=>"Start page", Refresh => $session{'home_refresh_interval'} &>
+<& /Elements/Tabs, current_toptab => '' &>
+<TABLE BORDER=0 WIDTH=100%>
+<TR VALIGN=TOP>
+<TD WIDTH=70%>
+<& /Elements/CustomHomepageHeader, %ARGS &>
+<& /Elements/MyTickets &>
+<BR>
+<& /Elements/MyRequests &>
+</TD>
+<TD>
+<& /Elements/Quicksearch &>
+<BR>
+<form method=get action="index.html">
+<& /Elements/Refresh, Name => 'HomeRefreshInterval', Default => $session {'home_refresh_interval'} &>
+<div align=right><input type=submit value="Go!"></div>
+</form>
+</TD>
+</TR>
+</TABLE>
+<%init>
+if ($ARGS{'HomeRefreshInterval'}) {
+       $session{'home_refresh_interval'} = $ARGS{'HomeRefreshInterval'};
+}
+</%init>
diff --git a/site_perl/Bill.pm b/site_perl/Bill.pm
deleted file mode 100644 (file)
index 4d7e059..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-package FS::Bill;
-
-use strict;
-use vars qw(@ISA);
-use FS::cust_main;
-
-@ISA = qw(FS::cust_main);
-
-warn "FS::Bill depriciated\n";
-
-=head1 NAME
-
-FS::Bill - Legacy stub
-
-=head1 SYNOPSIS
-
-The functionality of FS::Bill has been integrated into FS::cust_main.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-24 - 25 - 28
-
-use Safe; evaluate all fees with perl (still on TODO list until I write
-some examples & test opmask to see if we can read db)
-%hash=$obj->hash later ivan@sisd.com 98-mar-13
-
-packages with no next bill date start at $time not time, this should
-eliminate the last of the problems with billing at a past date
-also rewrite the invoice priting logic not to print invoices for things
-that haven't happended yet and update $cust_bill->printed when we print
-so PAST DUE notices work, and s/date/_date/ 
-ivan@sisd.com 98-jun-4
-
-more logic for past due stuff - packages with no next bill date start
-at $cust_pkg->setup || $time ivan@sisd.com 98-jul-13
-
-moved a few things in collection logic; negative charges should work
-ivan@sisd.com 98-aug-6
-
-pod, moved everything to FS::cust_main ivan@sisd.com 98-sep-19
-
-=cut
-
-1;
diff --git a/site_perl/CGI.pm b/site_perl/CGI.pm
deleted file mode 100644 (file)
index d2ed521..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-package FS::CGI;
-
-use strict;
-use vars qw(@EXPORT_OK @ISA);
-use Exporter;
-use CGI::Base;
-use CGI::Carp qw(fatalsToBrowser);
-
-@ISA = qw(Exporter);
-@EXPORT_OK = qw(header menubar idiot eidiot);
-
-=head1 NAME
-
-FS::CGI - Subroutines for the web interface
-
-=head1 SYNOPSIS
-
-  use FS::CGI qw(header menubar idiot eidiot);
-
-  print header( 'Title', '' );
-  print header( 'Title', menubar('item', 'URL', ... ) );
-
-  idiot "error message"; 
-  eidiot "error message";
-
-=head1 DESCRIPTION
-
-Provides a few common subroutines for the web interface.
-
-=head1 SUBROUTINES
-
-=over 4
-
-=item header TITLE, MENUBAR
-
-Returns an HTML header.
-
-=cut
-
-sub header {
-  my($title,$menubar)=@_;
-
-  <<END;
-    <HTML>
-      <HEAD>
-        <TITLE>
-          $title
-        </TITLE>
-      </HEAD>
-      <BODY>
-        <CENTER>
-          <H1>
-            $title
-          </H1>
-          $menubar
-        </CENTER>
-      <HR>
-END
-}
-
-=item menubar ITEM, URL, ...
-
-Returns an HTML menubar.
-
-=cut
-
-sub menubar { #$menubar=menubar('Main Menu', '../', 'Item', 'url', ... );
-  my($item,$url,@html);
-  while (@_) {
-    ($item,$url)=splice(@_,0,2);
-    push @html, qq!<A HREF="$url">$item</A>!;
-  }
-  join(' | ',@html);
-}
-
-=item idiot ERROR
-
-Sends headers and an HTML error message.
-
-=cut
-
-sub idiot {
-  my($error)=@_;
-  CGI::Base::SendHeaders();
-  print <<END;
-<HTML>
-  <HEAD>
-    <TITLE>Error processing your request</TITLE>
-  </HEAD>
-  <BODY>
-    <CENTER>
-    <H4>Error processing your request</H4>
-    </CENTER>
-    Your request could not be processed because of the following error:
-    <P><B>$error</B>
-    <P>Hit the <I>Back</I> button in your web browser, correct this mistake, and try again.
-  </BODY>
-</HTML>
-END
-
-}
-
-=item eidiot ERROR
-
-Sends headers and an HTML error message, then exits.
-
-=cut
-
-sub eidiot {
-  idiot(@_);
-  exit;
-}
-
-=back
-
-=head1 BUGS
-
-Not OO.
-
-Not complete.
-
-Uses CGI-modules instead of CGI.pm
-
-=head1 SEE ALSO
-
-L<CGI::Base>
-
-=head1 HISTORY
-
-subroutines for the HTML/CGI GUI, not properly OO. :(
-
-ivan@sisd.com 98-apr-16
-ivan@sisd.com 98-jun-22
-
-lose the background, eidiot ivan@sisd.com 98-sep-2
-
-pod ivan@sisd.com 98-sep-12
-
-=cut
-
-1;
-
-
diff --git a/site_perl/Conf.pm b/site_perl/Conf.pm
deleted file mode 100644 (file)
index d3ef307..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-package FS::Conf;
-
-use vars qw($default_dir);
-use IO::File;
-
-$default_dir='/var/spool/freeside/conf';
-
-=head1 NAME
-
-FS::Conf - Read access to Freeside configuration values
-
-=head1 SYNOPSIS
-
-  use FS::Conf;
-
-  $conf = new FS::Conf;
-  $conf = new FS::Conf "/non/standard/config/directory";
-
-  $dir = $conf->dir;
-
-  $value = $conf->config('key');
-  @list  = $conf->config('key');
-  $bool  = $conf->exists('key');
-
-=head1 DESCRIPTION
-
-Read access to Freeside configuration values.  Keys currently map to filenames,
-but this may change in the future.
-
-=head1 METHODS
-
-=over 4
-
-=item new [ DIRECTORY ]
-
-Create a new configuration object.  Optionally, a non-default directory may
-be specified.
-
-=cut
-
-sub new {
-  my($proto,$dir) = @_;
-  my($class) = ref($proto) || $proto;
-  my($self) = { 'dir' => $dir || $default_dir } ;
-  bless ($self, $class);
-}
-
-=item dir
-
-Returns the directory.
-
-=cut
-
-sub dir {
-  my($self) = @_;
-  $self->{dir};
-}
-
-=item config 
-
-Returns the configuration value or values (depending on context) for key.
-
-=cut
-
-sub config {
-  my($self,$file)=@_;
-  my($dir)=$self->dir;
-  my $fh = new IO::File "<$dir/$file" or return;
-  if ( wantarray ) {
-    map {
-      /^(.*)$/ or die "Illegal line in $dir/$file:\n$_\n";
-      $1;
-    } <$fh>;
-  } else {
-    <$fh> =~ /^(.*)$/ or die "Illegal line in $dir/$file:\n$_\n";
-    $1;
-  }
-}
-
-=item exists
-
-Returns true if the specified key exists, even if the corresponding value
-is undefined.
-
-=cut
-
-sub exists {
-  my($self,$file)=@_;
-  my($dir) = $self->dir;
-  -e "$dir/$file";
-}
-
-=back
-
-=head1 BUGS
-
-The option to specify a non-default directory should probably be removed.
-
-Write access (with locking) should be implemented.
-
-=head1 SEE ALSO
-
-config.html from the base documentation contains a list of configuration files.
-
-=head1 HISTORY
-
-Ivan Kohler <ivan@sisd.com> 98-sep-6
-
-sub exists forgot to fetch $dir ivan@sisd.com 98-sep-27
-
-=cut
-
-1;
diff --git a/site_perl/Invoice.pm b/site_perl/Invoice.pm
deleted file mode 100644 (file)
index 5eb596f..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-package FS::Invoice;
-
-use strict;
-use vars qw(@ISA);
-use FS::cust_bill;
-
-@ISA = qw(FS::cust_bill);
-
-#warn "FS::Invoice depriciated\n";
-
-=head1 NAME
-
-FS::Invoice - Legacy stub
-
-=head1 SYNOPSIS
-
-The functioanlity of FS::invoice has been integrated in FS::cust_bill.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jun-25 - 27
-
-maybe should be changed to be OO-functions on $cust_bill objects?
-(instead of passing invnum, ugh).
-
-ISA cust_bill and return inovice instead of passing filehandle
-ivan@sisd.com 98-mar-13
-(add postscript output!)
-
-close our kid when we're done ivan@sisd.com 98-jun-4
-
-separated code which shuffled data from code which formatted.
-(so i could) fixed past due notices showing up when balance due =< 0
-return address comes from /var/spool/freeside/conf/address
-ivan@sisd.com 98-jul-2
-
-pod ivan@sisd.com 98-sep-20something
-
-s/ISA/@ISA/ in use vars ivan@sisd.com 98-sep-27
-
-=cut
-
-1;
-
diff --git a/site_perl/Record.pm b/site_perl/Record.pm
deleted file mode 100644 (file)
index 9b30850..0000000
+++ /dev/null
@@ -1,868 +0,0 @@
-package FS::Record;
-
-use strict;
-use vars qw($dbdef_file $dbdef $setup_hack $AUTOLOAD @ISA @EXPORT_OK);
-use subs qw(reload_dbdef);
-use Exporter;
-use Carp;
-use File::CounterFile;
-use FS::UID qw(dbh checkruid swapuid getotaker datasrc);
-use FS::dbdef;
-
-@ISA = qw(Exporter);
-@EXPORT_OK = qw(dbh fields hfields qsearch qsearchs dbdef);
-
-$File::CounterFile::DEFAULT_DIR = "/var/spool/freeside/counters" ;
-
-$dbdef_file = "/var/spool/freeside/dbdef.". datasrc;
-
-reload_dbdef unless $setup_hack;
-
-=head1 NAME
-
-FS::Record - Database record objects
-
-=head1 SYNOPSIS
-
-    use FS::Record;
-    use FS::Record qw(dbh fields hfields qsearch qsearchs dbdef);
-
-    $record = new FS::Record 'table', \%hash;
-    $record = new FS::Record 'table', { 'column' => 'value', ... };
-
-    $record  = qsearchs FS::Record 'table', \%hash;
-    $record  = qsearchs FS::Record 'table', { 'column' => 'value', ... };
-    @records = qsearch  FS::Record 'table', \%hash; 
-    @records = qsearch  FS::Record 'table', { 'column' => 'value', ... };
-
-    $table = $record->table;
-    $dbdef_table = $record->dbdef_table;
-
-    $value = $record->get('column');
-    $value = $record->getfield('column');
-    $value = $record->column;
-
-    $record->set( 'column' => 'value' );
-    $record->setfield( 'column' => 'value' );
-    $record->column('value');
-
-    %hash = $record->hash;
-
-    $hashref = $record->hashref;
-
-    $error = $record->add;
-
-    $error = $record->del;
-
-    $error = $new_record->rep($old_record);
-
-    $value = $record->unique('column');
-
-    $value = $record->ut_float('column');
-    $value = $record->ut_number('column');
-    $value = $record->ut_numbern('column');
-    $value = $record->ut_money('column');
-    $value = $record->ut_text('column');
-    $value = $record->ut_textn('column');
-    $value = $record->ut_alpha('column');
-    $value = $record->ut_alphan('column');
-    $value = $record->ut_phonen('column');
-    $value = $record->ut_anythingn('column');
-
-    $dbdef = reload_dbdef;
-    $dbdef = reload_dbdef "/non/standard/filename";
-    $dbdef = dbdef;
-
-    $quoted_value = _quote($value,'table','field');
-
-    #depriciated
-    $fields = hfields('table');
-    if ( $fields->{Field} ) { # etc.
-
-    @fields = fields 'table';
-
-
-=head1 DESCRIPTION
-
-(Mostly) object-oriented interface to database records.  Records are currently
-implemented on top of DBI.  FS::Record is intended as a base class for
-table-specific classes to inherit from, i.e. FS::cust_main.
-
-=head1 METHODS
-
-=over 4
-
-=item new TABLE, HASHREF
-
-Creates a new record.  It doesn't store it in the database, though.  See
-L<"add"> for that.
-
-Note that the object stores this hash reference, not a distinct copy of the
-hash it points to.  You can ask the object for a copy with the I<hash> 
-method.
-
-=cut
-
-sub new { 
-  my($proto,$table,$hashref) = @_;
-  confess "Second arguement to FS::Record->new is not a HASH ref: ",
-          ref($hashref), " ", $hashref, "\n"
-    unless ref($hashref) eq 'HASH'; #bad practice?
-
-  #check to make sure $table exists? (ask dbdef)
-
-  foreach my $field ( FS::Record::fields $table ) { 
-     $hashref->{$field}='' unless defined $hashref->{$field};
-  }
-
-  # mySQL must rtrim the inbound text strings or store them z-terminated
-  # I simulate this for Postgres below
-  # Turned off in favor of ChopBlanks in UID.pm (see man DBI)
-  #if (datasrc =~ m/Pg/)
-  #{
-  #  foreach my $index (keys %$hashref)
-  #  {
-  #    $$hashref{$index} = unpack("A255", $$hashref{$index})
-  #    if ($$hashref{$index} =~ m/ $/) ;
-  #  }
-  #}
-
-  foreach my $column (keys %{$hashref}) {
-    #trim the '$' from money fields for Pg (beong HERE?)
-    #(what about Pg i18n?)
-    if ( datasrc =~ m/Pg/ 
-         && $dbdef->table($table)->column($column)->type eq 'money' ) {
-      ${$hashref}{$column} =~ s/^\$//;
-    }
-    #foreach my $column ( grep $dbdef->table($table)->column($_)->type eq 'money', keys %{$hashref} ) {
-    #  ${$hashref}{$column} =~ s/^\$//;
-    #}
-  }
-
-  my $class = ref($proto) || $proto;
-  my $self = { 'Table' => $table,
-               'Hash' => $hashref,
-             };
-
-  bless ($self, $class);
-
-}
-
-=item qsearch TABLE, HASHREF
-
-Searches the database for all records matching (at least) the key/value pairs
-in HASHREF.  Returns all the records found as FS::Record objects.
-
-=cut
-
-# Usage: @records = &FS::Search::qsearch($table,\%hash);
-# Each element of @records is a FS::Record object.
-sub qsearch {
-  my($table,$record) = @_;
-  my($dbh) = dbh;
-
-  my(@fields)=grep exists($record->{$_}), fields($table);
-
-  my($sth);
-  my($statement) = "SELECT * FROM $table". ( @fields
-    ? " WHERE ". join(' AND ',
-        map("$_ = ". _quote($record->{$_},$table,$_), @fields)
-      )
-    : ''
-  );
-  $sth=$dbh->prepare($statement)
-    or croak $dbh->errstr; #is that a little too harsh?  hmm.
-
-  map {
-    new FS::Record ($table,$sth->fetchrow_hashref);
-  } ( 1 .. $sth->execute );
-
-}
-
-=item qsearchs TABLE, HASHREF
-
-Searches the database for a record matching (at least) the key/value pairs
-in HASHREF, and returns the record found as an FS::Record object.  If more than
-one record matches, it B<carp>s but returns the first.  If this happens, you
-either made a logic error in asking for a single item, or your data is
-corrupted.
-
-=cut
-
-sub qsearchs { # $result_record = &FS::Record:qsearchs('table',\%hash);
-  my(@result) = qsearch(@_);
-  carp "Multiple records in scalar search!" if scalar(@result) > 1;
-    #should warn more vehemently if the search was on a primary key?
-  $result[0];
-}
-
-=item table
-
-Returns the table name.
-
-=cut
-
-sub table {
-  my($self) = @_;
-  $self -> {'Table'};
-}
-
-=item dbdef_table
-
-Returns the FS::dbdef_table object for the table.
-
-=cut
-
-sub dbdef_table {
-  my($self)=@_;
-  my($table)=$self->table;
-  $dbdef->table($table);
-}
-
-=item get, getfield COLUMN
-
-Returns the value of the column/field/key COLUMN.
-
-=cut
-
-sub get {
-  my($self,$field) = @_;
-  # to avoid "Use of unitialized value" errors
-  if ( defined ( $self->{Hash}->{$field} ) ) {
-    $self->{Hash}->{$field};
-  } else { 
-    '';
-  }
-}
-sub getfield {
-  get(@_);
-}
-
-=item set, setfield COLUMN, VALUE
-
-Sets the value of the column/field/key COLUMN to VALUE.  Returns VALUE.
-
-=cut
-
-sub set { 
-  my($self,$field,$value) = @_;
-  $self->{'Hash'}->{$field} = $value;
-}
-sub setfield {
-  set(@_);
-}
-
-=item AUTLOADED METHODS
-
-$record->column is a synonym for $record->get('column');
-
-$record->column('value') is a synonym for $record->set('column','value');
-
-=cut
-
-sub AUTOLOAD {
-  my($self,$value)=@_;
-  my($field)=$AUTOLOAD;
-  $field =~ s/.*://;
-  if ( defined($value) ) {
-    $self->setfield($field,$value);
-  } else {
-    $self->getfield($field);
-  }    
-}
-
-=item hash
-
-Returns a list of the column/value pairs, usually for assigning to a new hash.
-
-To make a distinct duplicate of an FS::Record object, you can do:
-
-    $new = new FS::Record ( $old->table, { $old->hash } );
-
-=cut
-
-sub hash {
-  my($self) = @_;
-  %{ $self->{'Hash'} }; 
-}
-
-=item hashref
-
-Returns a reference to the column/value hash.
-
-=cut
-
-sub hashref {
-  my($self) = @_;
-  $self->{'Hash'};
-}
-
-=item add
-
-Adds this record to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub add {
-  my($self) = @_;
-  my($dbh)=dbh;
-  my($table)=$self->table;
-
-  #single-field unique keys are given a value if false
-  #(like MySQL's AUTO_INCREMENT)
-  foreach ( $dbdef->table($table)->unique->singles ) {
-    $self->unique($_) unless $self->getfield($_);
-  }
-  #and also the primary key
-  my($primary_key)=$dbdef->table($table)->primary_key;
-  $self->unique($primary_key) 
-    if $primary_key && ! $self->getfield($primary_key);
-
-  my (@fields) =
-    grep defined($self->getfield($_)) && $self->getfield($_) ne "",
-    fields($table)
-  ;
-
-  my($sth);
-  my($statement)="INSERT INTO $table ( ".
-      join(', ',@fields ).
-    ") VALUES (".
-      join(', ',map(_quote($self->getfield($_),$table,$_), @fields)).
-    ")"
-  ;
-  $sth = $dbh->prepare($statement) or return $dbh->errstr;
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE'; 
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  $sth->execute or return $sth->errstr;
-
-  '';
-}
-
-=item del
-
-Delete this record from the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub del {
-  my($self) = @_;
-  my($dbh)=dbh;
-  my($table)=$self->table;
-
-  my($sth);
-  my($statement)="DELETE FROM $table WHERE ". join(' AND ',
-    map {
-      $self->getfield($_) eq ''
-        ? "$_ IS NULL"
-        : "$_ = ". _quote($self->getfield($_),$table,$_)
-    } ( $dbdef->table($table)->primary_key )
-          ? ($dbdef->table($table)->primary_key)
-          : fields($table)
-  );
-  $sth = $dbh->prepare($statement) or return $dbh->errstr;
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE'; 
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  my($rc);
-  $rc=$sth->execute or return $sth->errstr;
-  #not portable #return "Record not found, statement:\n$statement" if $rc eq "0E0";
-
-  undef $self; #no need to keep object!
-
-  '';
-}
-
-=item rep OLD_RECORD
-
-Replace the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub rep {
-  my($new,$old)=@_;
-  my($dbh)=dbh;
-  my($table)=$old->table;
-  my(@fields)=fields($table);
-  my(@diff)=grep $new->getfield($_) ne $old->getfield($_), @fields;
-
-  if ( scalar(@diff) == 0 ) {
-    carp "Records identical";
-    return '';
-  }
-
-  return "Records not in same table!" unless $new->table eq $table;
-
-  my($sth);
-  my($statement)="UPDATE $table SET ". join(', ',
-    map {
-      "$_ = ". _quote($new->getfield($_),$table,$_) 
-    } @diff
-  ). ' WHERE '.
-    join(' AND ',
-      map {
-        $old->getfield($_) eq ''
-          ? "$_ IS NULL"
-          : "$_ = ". _quote($old->getfield($_),$table,$_)
-#      } @fields
-#      } ( primary_key($table) ? (primary_key($table)) : @fields )
-      } ( $dbdef->table($table)->primary_key 
-            ? ($dbdef->table($table)->primary_key)
-            : @fields
-        )
-    )
-  ;
-  #warn $statement;
-  $sth = $dbh->prepare($statement) or return $dbh->errstr;
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE'; 
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  my($rc);
-  $rc=$sth->execute or return $sth->errstr;
-  #not portable #return "Record not found (or records identical)." if $rc eq "0E0";
-
-  '';
-
-}
-
-=item unique COLUMN
-
-Replaces COLUMN in record with a unique number.  Called by the B<add> method
-on primary keys and single-field unique columns (see L<FS::dbdef_table>).
-Returns the new value.
-
-=cut
-
-sub unique {
-  my($self,$field) = @_;
-  my($table)=$self->table;
-
-  croak("&FS::UID::checkruid failed") unless &checkruid;
-
-  croak "Unique called on field $field, but it is ",
-        $self->getfield($field),
-        ", not null!"
-    if $self->getfield($field);
-
-  #warn "table $table is tainted" if is_tainted($table);
-  #warn "field $field is tainted" if is_tainted($field);
-
-  &swapuid;
-  my($counter) = new File::CounterFile "$table.$field",0;
-# hack for web demo
-#  getotaker() =~ /^([\w\-]{1,16})$/ or die "Illegal CGI REMOTE_USER!";
-#  my($user)=$1;
-#  my($counter) = new File::CounterFile "$user/$table.$field",0;
-# endhack
-
-  my($index)=$counter->inc;
-  $index=$counter->inc
-    while qsearchs($table,{$field=>$index}); #just in case
-  &swapuid;
-
-  $index =~ /^(\d*)$/;
-  $index=$1;
-
-  $self->setfield($field,$index);
-
-}
-
-=item ut_float COLUMN
-
-Check/untaint floating point numeric data: 1.1, 1, 1.1e10, 1e10.  May not be
-null.  If there is an error, returns the error, otherwise returns false.
-
-=cut
-
-sub ut_float {
-  my($self,$field)=@_ ;
-  ($self->getfield($field) =~ /^(\d+\.\d+)$/ ||
-   $self->getfield($field) =~ /^(\d+)$/ ||
-   $self->getfield($field) =~ /^(\d+\.\d+e\d+)$/ ||
-   $self->getfield($field) =~ /^(\d+e\d+)$/)
-    or return "Illegal or empty (float) $field!";
-  $self->setfield($field,$1);
-  '';
-}
-
-=item ut_number COLUMN
-
-Check/untaint simple numeric data (whole numbers).  May not be null.  If there
-is an error, returns the error, otherwise returns false.
-
-=cut
-
-sub ut_number {
-  my($self,$field)=@_;
-  $self->getfield($field) =~ /^(\d+)$/
-    or return "Illegal or empty (numeric) $field!";
-  $self->setfield($field,$1);
-  '';
-}
-
-=item ut_numbern COLUMN
-
-Check/untaint simple numeric data (whole numbers).  May be null.  If there is
-an error, returns the error, otherwise returns false.
-
-=cut
-
-sub ut_numbern {
-  my($self,$field)=@_;
-  $self->getfield($field) =~ /^(\d*)$/
-    or return "Illegal (numeric) $field!";
-  $self->setfield($field,$1);
-  '';
-}
-
-=item ut_money COLUMN
-
-Check/untaint monetary numbers.  May be negative.  Set to 0 if null.  If there
-is an error, returns the error, otherwise returns false.
-
-=cut
-
-sub ut_money {
-  my($self,$field)=@_;
-  $self->getfield($field) =~ /^(\-)? ?(\d*)(\.\d{2})?$/
-    or return "Illegal (money) $field!";
-  $self->setfield($field,"$1$2$3" || 0);
-  '';
-}
-
-=item ut_text COLUMN
-
-Check/untaint text.  Alphanumerics, spaces, and the following punctuation
-symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? /
-May not be null.  If there is an error, returns the error, otherwise returns
-false.
-
-=cut
-
-sub ut_text {
-  my($self,$field)=@_;
-  $self->getfield($field) =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/]+)$/
-    or return "Illegal or empty (text) $field";
-  $self->setfield($field,$1);
-  '';
-}
-
-=item ut_textn COLUMN
-
-Check/untaint text.  Alphanumerics, spaces, and the following punctuation
-symbols are currently permitted: ! @ # $ % & ( ) - + ; : ' " , . ? /
-May be null.  If there is an error, returns the error, otherwise returns false.
-
-=cut
-
-sub ut_textn {
-  my($self,$field)=@_;
-  $self->getfield($field) =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/]*)$/
-    or return "Illegal (text) $field";
-  $self->setfield($field,$1);
-  '';
-}
-
-=item ut_alpha COLUMN
-
-Check/untaint alphanumeric strings (no spaces).  May not be null.  If there is
-an error, returns the error, otherwise returns false.
-
-=cut
-
-sub ut_alpha {
-  my($self,$field)=@_;
-  $self->getfield($field) =~ /^(\w+)$/
-    or return "Illegal or empty (alphanumeric) $field!";
-  $self->setfield($field,$1);
-  '';
-}
-
-=item ut_alpha COLUMN
-
-Check/untaint alphanumeric strings (no spaces).  May be null.  If there is an
-error, returns the error, otherwise returns false.
-
-=cut
-
-sub ut_alphan {
-  my($self,$field)=@_;
-  $self->getfield($field) =~ /^(\w*)$/ 
-    or return "Illegal (alphanumeric) $field!";
-  $self->setfield($field,$1);
-  '';
-}
-
-=item ut_phonen COLUMN
-
-Check/untaint phone numbers.  May be null.  If there is an error, returns
-the error, otherwise returns false.
-
-=cut
-
-sub ut_phonen {
-  my($self,$field)=@_;
-  my $phonen = $self->getfield($field);
-  if ( $phonen eq '' ) {
-    $self->setfield($field,'');
-  } else {
-    $phonen =~ s/\D//g;
-    $phonen =~ /^(\d{3})(\d{3})(\d{4})(\d*)$/
-      or return "Illegal (phone) $field!";
-    $phonen = "$1-$2-$3";
-    $phonen .= " x$4" if $4;
-    $self->setfield($field,$phonen);
-  }
-  '';
-}
-
-=item ut_anything COLUMN
-
-Untaints arbitrary data.  Be careful.
-
-=cut
-
-sub ut_anything {
-  my($self,$field)=@_;
-  $self->getfield($field) =~ /^(.*)$/ or return "Illegal $field!";
-  $self->setfield($field,$1);
-  '';
-}
-
-
-=head1 SUBROUTINES
-
-=over 4
-
-=item reload_dbdef([FILENAME])
-
-Load a database definition (see L<FS::dbdef>), optionally from a non-default
-filename.  This command is executed at startup unless
-I<$FS::Record::setup_hack> is true.  Returns a FS::dbdef object.
-
-=cut
-
-sub reload_dbdef {
-  my $file = shift || $dbdef_file;
-  $dbdef = load FS::dbdef ($file);
-}
-
-=item dbdef
-
-Returns the current database definition.  See L<FS::dbdef>.
-
-=cut
-
-sub dbdef { $dbdef; }
-
-=item _quote VALUE, TABLE, COLUMN
-
-This is an internal function used to construct SQL statements.  It returns
-VALUE DBI-quoted (see L<DBI/"quote">) unless VALUE is a number and the column
-type (see L<dbdef_column>) does not end in `char' or `binary'.
-
-=cut
-
-sub _quote {
-  my($value,$table,$field)=@_;
-  my($dbh)=dbh;
-  if ( $value =~ /^\d+(\.\d+)?$/ && 
-#       ! ( datatype($table,$field) =~ /^char/ ) 
-       ! ( $dbdef->table($table)->column($field)->type =~ /(char|binary)$/i ) 
-  ) {
-    $value;
-  } else {
-    $dbh->quote($value);
-  }
-}
-
-=item hfields TABLE
-
-This is depriciated.  Don't use it.
-
-It returns a hash-type list with the fields of this record's table set true.
-
-=cut
-
-sub hfields {
-  carp "hfields is depriciated";
-  my($table)=@_;
-  my(%hash);
-  foreach (fields($table)) {
-    $hash{$_}=1;
-  }
-  \%hash;
-}
-
-=item fields TABLE
-
-This returns a list of the columns in this record's table
-(See L<dbdef_table>).
-
-=cut
-
-# Usage: @fields = fields($table);
-sub fields {
-  my($table) = @_;
-  #my(@fields) = $dbdef->table($table)->columns;
-  croak "Usage: \@fields = fields(\$table)" unless $table;
-  my($table_obj) = $dbdef->table($table);
-  croak "Unknown table $table" unless $table_obj;
-  $table_obj->columns;
-}
-
-#sub _dump {
-#  my($self)=@_;
-#  join("\n", map {
-#    "$_: ". $self->getfield($_). "|"
-#  } (fields($self->table)) );
-#}
-
-#sub DESTROY {
-#  my $self = shift;
-#  #use Carp qw(cluck);
-#  #cluck "DESTROYING $self";
-#  warn "DESTROYING $self";
-#}
-
-#sub is_tainted {
-#             return ! eval { join('',@_), kill 0; 1; };
-#         }
-
-=back
-
-=head1 BUGS
-
-This module should probably be renamed, since much of the functionality is
-of general use.  It is not completely unlike Adapter::DBI (see below).
-
-Exported qsearch and qsearchs should be depriciated in favor of method calls
-(against an FS::Record object like the old search and searchs that qsearch
-and qsearchs were on top of.)
-
-The whole fields / hfields mess should be removed.
-
-The various WHERE clauses should be subroutined.
-
-table string should be depriciated in favor of FS::dbdef_table.
-
-No doubt we could benefit from a Tied hash.  Documenting how exists / defined
-true maps to the database (and WHERE clauses) would also help.
-
-The ut_ methods should ask the dbdef for a default length.
-
-ut_sqltype (like ut_varchar) should all be defined
-
-A fallback check method should be provided with uses the dbdef.
-
-The ut_money method assumes money has two decimal digits.
-
-The Pg money kludge in the new method only strips `$'.
-
-The ut_phonen method assumes US-style phone numbers.
-
-The _quote function should probably use ut_float instead of a regex.
-
-All the subroutines probably should be methods, here or elsewhere.
-
-=head1 SEE ALSO
-
-L<FS::dbdef>, L<FS::UID>, L<DBI>
-
-Adapter::DBI from Ch. 11 of Advanced Perl Programming by Sriram Srinivasan.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jun-2 - 9, 19, 25, 27, 30
-
-DBI version
-ivan@sisd.com 97-nov-8 - 12
-
-cleaned up, added autoloaded $self->any_field calls, moved DBI login stuff
-to FS::UID
-ivan@sisd.com 97-nov-21-23
-
-since AUTO_INCREMENT is MySQL specific, use my own unique number generator
-(again)
-ivan@sisd.com 97-dec-4
-
-untaint $user in unique (web demo hack...bah)
-make unique skip multiple-field unique's from dbdef
-ivan@sisd.com 97-dec-11
-
-merge with FS::Search, which after all was just alternate constructors for
-FS::Record objects.  Makes lots of things cleaner.  :)
-ivan@sisd.com 97-dec-13
-
-use FS::dbdef::primary key in replace searches, hopefully for all practical 
-purposes the string/number problem in SQL statements should be gone?
-(SQL bites)
-ivan@sisd.com 98-jan-20
-
-Put all SQL statments in $statment before we $sth=$dbh->prepare( them,
-for debugging reasons (warn $statement) ivan@sisd.com 98-feb-19
-
-(sigh)... use dbdef type (char, etc.) instead of a regex to decide
-what to quote in _quote (more sillines...)  SQL bites.
-ivan@sisd.com 98-feb-20
-
-more friendly error messages ivan@sisd.com 98-mar-13
-
-Added import of datasrc from FS::UID to allow Pg6.3 to work
-Added code to right-trim strings read from Pg6.3 databases
-Modified 'add' to only insert fields that actually have data
-Added ut_float to handle floating point numbers (for sales tax).
-Pg6.3 does not have a "SHOW FIELDS" statement, so I faked it 8).
-       bmccane@maxbaud.net     98-apr-3
-
-commented out Pg wrapper around `` Modified 'add' to only insert fields that
-actually have data '' ivan@sisd.com 98-apr-16
-
-dbdef usage changes ivan@sisd.com 98-jun-1
-
-sub fields now asks dbdef, not database ivan@sisd.com 98-jun-2
-
-added debugging method ->_dump ivan@sisd.com 98-jun-16
-
-use FS::dbdef::primary key in delete searches as well as replace
-searches (SQL still bites) ivan@sisd.com 98-jun-22
-
-sub dbdef_table ivan@sisd.com 98-jun-28
-
-removed Pg wrapper around `` Modified 'add' to only insert fields that
-actually have data '' ivan@sisd.com 98-jul-14
-
-sub fields croaks on errors ivan@sisd.com 98-jul-17
-
-$rc eq '0E0' doesn't mean we couldn't delete for all rdbmss 
-ivan@sisd.com 98-jul-18
-
-commented out code to right-trim strings read from Pg6.3 databases;
-ChopBlanks is in UID.pm ivan@sisd.com 98-aug-16
-
-added code (with Pg wrapper) to deal with Pg money fields
-ivan@sisd.com 98-aug-18
-
-added pod documentation ivan@sisd.com 98-sep-6
-
-ut_phonen got ''; at the end ivan@sisd.com 98-sep-27
-
-=cut
-
-1;
-
diff --git a/site_perl/SSH.pm b/site_perl/SSH.pm
deleted file mode 100644 (file)
index d5a0df6..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-package FS::SSH;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK $ssh $scp);
-use Exporter;
-use IPC::Open2;
-use IPC::Open3;
-
-@ISA = qw(Exporter);
-@EXPORT_OK = qw(ssh scp issh iscp sshopen2 sshopen3);
-
-$ssh="ssh";
-$scp="scp";
-
-=head1 NAME
-
-FS::SSH - Subroutines to call ssh and scp
-
-=head1 SYNOPSIS
-
-  use FS::SSH qw(ssh scp issh iscp sshopen2 sshopen3);
-
-  ssh($host, $command);
-
-  issh($host, $command);
-
-  scp($source, $destination);
-
-  iscp($source, $destination);
-
-  sshopen2($host, $reader, $writer, $command);
-
-  sshopen3($host, $reader, $writer, $error, $command);
-
-=head1 DESCRIPTION
-
-  Simple wrappers around ssh and scp commands.
-
-=head1 SUBROUTINES
-
-=over 4
-
-=item ssh HOST, COMMAND 
-
-Calls ssh in batch mode.
-
-=cut
-
-sub ssh {
-  my($host,$command)=@_;
-  my(@cmd)=($ssh, "-o", "BatchMode yes", $host, $command);
-#      print join(' ',@cmd),"\n";
-#0;
-  system(@cmd);
-}
-
-=item issh HOST, COMMAND
-
-Prints the ssh command to be executed, waits for the user to confirm, and
-(optionally) executes the command.
-
-=cut
-
-sub issh {
-  my($host,$command)=@_;
-  my(@cmd)=($ssh, $host, $command);
-  print join(' ',@cmd),"\n";
-  if ( &_yesno ) {
-       ###print join(' ',@cmd),"\n";
-    system(@cmd);
-  }
-}
-
-=item scp SOURCE, DESTINATION
-
-Calls scp in batch mode.
-
-=cut
-
-sub scp {
-  my($src,$dest)=@_;
-  my(@cmd)=($scp,"-Bprq",$src,$dest);
-#      print join(' ',@cmd),"\n";
-#0;
-  system(@cmd);
-}
-
-=item iscp SOURCE, DESTINATION
-
-Prints the scp command to be executed, waits for the user to confirm, and
-(optionally) executes the command.
-
-=cut
-
-sub iscp {
-  my($src,$dest)=@_;
-  my(@cmd)=($scp,"-pr",$src,$dest);
-  print join(' ',@cmd),"\n";
-  if ( &_yesno ) {
-       ###print join(' ',@cmd),"\n";
-    system(@cmd);
-  }
-}
-
-=item sshopen2 HOST, READER, WRITER, COMMAND
-
-Connects the supplied filehandles to the ssh process (in batch mode).
-
-=cut
-
-sub sshopen2 {
-  my($host,$reader,$writer,$command)=@_;
-  open2($reader,$writer,$ssh,'-o','Batchmode yes',$host,$command);
-}
-
-=item sshopen3 HOST, WRITER, READER, ERROR, COMMAND
-
-Connects the supplied filehandles to the ssh process (in batch mode).
-
-=cut
-
-sub sshopen3 {
-  my($host,$writer,$reader,$error,$command)=@_;
-  open3($writer,$reader,$error,$ssh,'-o','Batchmode yes',$host,$command);
-}
-
-sub _yesno {
-  print "Proceed [y/N]:";
-  my($x)=scalar(<STDIN>);
-  $x =~ /^y/i;
-}
-
-=head1 BUGS
-
-Not OO.
-
-scp stuff should transparantly use rsync-over-ssh instead.
-
-=head1 SEE ALSO
-
-L<ssh>, L<scp>, L<IPC::Open2>, L<IPC::Open3>
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-17
-
-added sshopen2 and sshopen3 ivan@sisd.com 98-mar-9
-
-added iscp ivan@sisd.com 98-jul-25
-now iscp asks y/n, issh and took out path ivan@sisd.com 98-jul-30
-
-pod ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/UID.pm b/site_perl/UID.pm
deleted file mode 100644 (file)
index 16f03a0..0000000
+++ /dev/null
@@ -1,209 +0,0 @@
-package FS::UID;
-
-use strict;
-use vars qw(
-  @ISA @EXPORT_OK $cgi $dbh $freeside_uid $conf $datasrc $db_user $db_pass
-);
-use Exporter;
-use Carp;
-use DBI;
-use FS::Conf;
-
-@ISA = qw(Exporter);
-@EXPORT_OK = qw(checkeuid checkruid swapuid cgisuidsetup
-                adminsuidsetup getotaker dbh datasrc);
-
-$freeside_uid = scalar(getpwnam('freeside'));
-
-my $conf = new FS::Conf;
-($datasrc, $db_user, $db_pass) = $conf->config('secrets')
-  or die "Can't get secrets: $!";
-
-=head1 NAME
-
-FS::UID - Subroutines for database login and assorted other stuff
-
-=head1 SYNOPSIS
-
-  use FS::UID qw(adminsuidsetup cgisuidsetup dbh datasrc getotaker
-  checkeuid checkruid swapuid);
-
-  adminsuidsetup;
-
-  $cgi = new CGI::Base;
-  $cgi->get;
-  $dbh = cgisuidsetup($cgi);
-
-  $dbh = dbh;
-
-  $datasrc = datasrc;
-
-=head1 DESCRIPTION
-
-Provides a hodgepodge of subroutines. 
-
-=head1 SUBROUTINES
-
-=over 4
-
-=item adminsuidsetup
-
-Cleans the environment.
-Make sure the script is running as freeside, or setuid freeside.
-Opens a connection to the database.
-Swaps real and effective UIDs.
-Returns the DBI database handle (usually you don't need this).
-
-=cut
-
-sub adminsuidsetup {
-
-  $ENV{'PATH'} ='/usr/local/bin:/usr/bin:/usr/ucb:/bin';
-  $ENV{'SHELL'} = '/bin/sh';
-  $ENV{'IFS'} = " \t\n";
-  $ENV{'CDPATH'} = '';
-  $ENV{'ENV'} = '';
-  $ENV{'BASH_ENV'} = '';
-
-  croak "Not running uid freeside!" unless checkeuid();
-  $dbh = DBI->connect($datasrc,$db_user,$db_pass, {
-       # hack for web demo
-       #  my($user)=getotaker();
-       #  $dbh = DBI->connect("$datasrc:$user",$db_user,$db_pass, {
-                          'AutoCommit' => 'true',
-                          'ChopBlanks' => 'true',
-  } ) or die "DBI->connect error: $DBI::errstr\n";;
-
-  swapuid(); #go to non-privledged user if running setuid freeside
-
-  $dbh;
-}
-=item cgisuidsetup CGI::Base_OBJECT
-
-Stores the CGI::Base_OBJECT for later use.
-Runs adminsuidsetup.
-
-=cut
-
-sub cgisuidsetup {
-  $cgi=$_[0];
-  adminsuidsetup;
-}
-
-=item dbh
-
-Returns the DBI database handle.
-
-=cut
-
-sub dbh {
-  $dbh;
-}
-
-=item datasrc
-
-Returns the DBI data source.
-
-=cut
-
-sub datasrc {
-  $datasrc;
-}
-
-#hack for web demo
-#sub setdbh {
-#  $dbh=$_[0];
-#}
-
-sub suidsetup {
-  croak "suidsetup depriciated";
-}
-
-=item getotaker
-
-Returns the current Freeside user.  Currently that means the CGI REMOTE_USER,
-or 'freeside'.
-
-=cut
-
-sub getotaker {
-  if ($cgi && defined $cgi->var('REMOTE_USER')) {
-    return $cgi->var('REMOTE_USER'); #for now
-  } else {
-    'freeside';
-  }
-}
-
-=item checkeuid
-
-Returns true if effective UID is that of the freeside user.
-
-=cut
-
-sub checkeuid {
-  ( $> == $freeside_uid );
-}
-
-=item checkruid
-
-Returns true if the real UID is that of the freeside user.
-
-=cut
-
-sub checkruid {
-  ( $< == $freeside_uid );
-}
-
-=item swapuid
-
-Swaps real and effective UIDs.
-
-=cut
-
-sub swapuid {
-  ($<,$>) = ($>,$<);
-}
-
-=back
-
-=head1 BUGS
-
-Not OO.
-
-No capabilities yet.  When mod_perl and Authen::DBI are implemented, 
-cgisuidsetup will go away as well.
-
-=head1 SEE ALSO
-
-L<FS::Record>,  L<CGI::Base>, L<DBI>
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jun-4 - 9
-untaint otaker ivan@voicenet.com 97-jul-7
-
-generalize and auto-get uid (getotaker still needs to be db'ed)
-ivan@sisd.com 97-nov-10
-
-&cgisuidsetup logs into database.  other cleaning.
-ivan@sisd.com 97-nov-22,23
-
-&adminsuidsetup logs into database with otaker='freeside' (for
-automated tasks like billing)
-ivan@sisd.com 97-dec-13
-
-added sub datasrc for fs-setup ivan@sisd.com 98-feb-21
-
-datasrc, user and pass now come from conf/secrets ivan@sisd.com 98-jun-28
-
-added ChopBlanks to DBI call (see man DBI) ivan@sisd.com 98-aug-16
-
-pod, use FS::Conf, implemented cgisuidsetup as adminsuidsetup,
-inlined suidsetup
-ivan@sisd.com 98-sep-12
-
-=cut
-
-1;
-
diff --git a/site_perl/agent.pm b/site_perl/agent.pm
deleted file mode 100644 (file)
index 7fc370e..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-package FS::agent;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::Record qw(fields qsearch qsearchs);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-=head1 NAME
-
-FS::agent - Object methods for agent records
-
-=head1 SYNOPSIS
-
-  use FS::agent;
-
-  $record = create FS::agent \%hash;
-  $record = create FS::agent { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::agent object represents an agent.  Every customer has an agent.  Agents
-can be used to track things like resellers or salespeople.  FS::agent inherits
-from FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item agemtnum - primary key (assigned automatically for new agents)
-
-=item agent - Text name of this agent
-
-=item typenum - Agent type.  See L<FS::agent_type>
-
-=item prog - For future use.
-
-=item freq - For future use.
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new agent.  To add the agent to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('agent')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('agent',$hashref);
-}
-
-=item insert
-
-Adds this agent to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Deletes this agent from the database.  Only agents with no customers can be
-deleted.  If there is an error, returns the error, otherwise returns false.
-
-=cut
-
-sub delete {
-  my($self)=@_;
-  return "Can't delete an agent with customers!"
-    if qsearch('cust_main',{'agentnum' => $self->agentnum});
-  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not an agent record!" unless $old->table eq "agent";
-  return "Can't change agentnum!"
-    unless $old->getfield('agentnum') eq $new->getfield('agentnum');
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid agent.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert and replace
-methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a agent record!" unless $self->table eq "agent";
-
-  my($error)=
-    $self->ut_numbern('agentnum')
-      or $self->ut_text('agent')
-      or $self->ut_number('typenum')
-      or $self->ut_numbern('freq')
-      or $self->ut_textn('prog')
-  ;
-  return $error if $error;
-
-  return "Unknown typenum!"
-    unless qsearchs('agent_type',{'typenum'=> $self->getfield('typenum') });
-
-  '';
-
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::agent_type>, L<FS::cust_main>, schema.html from the base
-documentation.
-
-=head1 HISTORY
-
-Class dealing with agent (resellers)
-
-ivan@sisd.com 97-nov-13, 97-dec-10
-
-pod, added check in ->delete ivan@sisd.com 98-sep-22
-
-=cut
-
-1;
-
diff --git a/site_perl/agent_type.pm b/site_perl/agent_type.pm
deleted file mode 100644 (file)
index 002c36f..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-package FS::agent_type;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::Record qw(qsearch fields);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-=head1 NAME
-
-FS::agent_type - Object methods for agent_type records
-
-=head1 SYNOPSIS
-
-  use FS::agent_type;
-
-  $record = create FS::agent_type \%hash;
-  $record = create FS::agent_type { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::agent_type object represents an agent type.  Every agent (see
-L<FS::agent>) has an agent type.  Agent types define which packages (see
-L<FS::part_pkg>) may be purchased by customers (see L<FS::cust_main>), via 
-FS::type_pkgs records (see L<FS::type_pkgs>).  FS::agent_type inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item typenum - primary key (assigned automatically for new agent types)
-
-=item atype - Text name of this agent type
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new agent type.  To add the agent type to the database, see
-L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('agent_type')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('agent_type',$hashref);
-
-}
-
-=item insert
-
-Adds this agent type to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Deletes this agent type from the database.  Only agent types with no agents
-can be deleted.  If there is an error, returns the error, otherwise returns
-false.
-
-=cut
-
-sub delete {
-  my($self)=@_;
-  return "Can't delete an agent_type with agents!"
-    if qsearch('agent',{'typenum' => $self->typenum});
-  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a agent_type record!" unless $old->table eq "agent_type";
-  return "Can't change typenum!"   
-    unless $old->getfield('typenum') eq $new->getfield('typenum');
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid agent type.  If there is an
-error, returns the error, otherwise returns false.  Called by the insert and
-replace methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a agent_type record!" unless $self->table eq "agent_type";
-
-  $self->ut_numbern('typenum')
-  or $self->ut_text('atype');
-
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::agent>, L<FS::type_pkgs>, L<FS::cust_main>,
-L<FS::part_pkg>, schema.html from the base documentation.
-
-=head1 HISTORY
-
-Class for the different sets of allowable packages you can assign to an
-agent.
-
-ivan@sisd.com 97-nov-13
-
-ut_ FS::Record methods
-ivan@sisd.com 97-dec-10
-
-Changed 'type' to 'atype' because Pg6.3 reserves the type word
-       bmccane@maxbaud.net     98-apr-3
-
-pod, added check in delete ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/cust_bill.pm b/site_perl/cust_bill.pm
deleted file mode 100644 (file)
index 0023451..0000000
+++ /dev/null
@@ -1,495 +0,0 @@
-package FS::cust_bill;
-
-use strict;
-use vars qw(@ISA $conf $add1 $add2 $add3 $add4);
-use Exporter;
-use Date::Format;
-use FS::Record qw(fields qsearch qsearchs);
-
-@ISA = qw(FS::Record Exporter);
-
-$conf = new FS::Conf;
-
-($add1,$add2,$add3,$add4) = $conf->config('address');
-
-=head1 NAME
-
-FS::cust_bill - Object methods for cust_bill records
-
-=head1 SYNOPSIS
-
-  use FS::cust_bill;
-
-  $record = create FS::cust_bill \%hash;
-  $record = create FS::cust_bill { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-  ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
-
-  @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
-
-  ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
-
-  @cust_pay_objects = $cust_bill->cust_pay;
-
-  @lines = $cust_bill->print_text;
-  @lines = $cust_bill->print_text $time;
-
-=head1 DESCRIPTION
-
-An FS::cust_bill object represents an invoice.  FS::cust_bill inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item invnum - primary key (assigned automatically for new invoices)
-
-=item custnum - customer (see L<FS::cust_main>)
-
-=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-=item charged - amount of this invoice
-
-=item owed - amount still outstanding on this invoice, which is charged minus
-all payments (see L<FS::cust_pay>).
-
-=item printed - how many times this invoice has been printed automatically
-(see L<FS::cust_main/"collect">).
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new invoice.  To add the invoice to the database, see L<"insert">.
-Invoices are normally created by calling the bill method of a customer object
-(see L<FS::cust_main>).
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('cust_bill')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('cust_bill',$hashref);
-}
-
-=item insert
-
-Adds this invoice to the database ("Posts" the invoice).  If there is an error,
-returns the error, otherwise returns false.
-
-When adding new invoices, owed must be charged (or null, in which case it is
-automatically set to charged).
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->setfield('owed',$self->charged) if $self->owed eq '';
-  return "owed != charged!"
-    unless $self->owed == $self->charged;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented.  I don't remove invoices because there would then be
-no record you ever posted this invoice (which is bad, no?)
-
-=cut
-
-sub delete {
-  return "Can't remove invoice!"
-  #my($self)=@_;
-  #$self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-Only owed and printed may be changed.  Owed is normally updated by creating and
-inserting a payment (see L<FS::cust_pay>).  Printed is normally updated by
-calling the collect method of a customer object (see L<FS::cust_main>).
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a cust_bill record!" unless $old->table eq "cust_bill";
-  return "Can't change invnum!"
-    unless $old->getfield('invnum') eq $new->getfield('invnum');
-  return "Can't change custnum!"
-    unless $old->getfield('custnum') eq $new->getfield('custnum');
-  return "Can't change _date!"
-    unless $old->getfield('_date') eq $new->getfield('_date');
-  return "Can't change charged!"
-    unless $old->getfield('charged') eq $new->getfield('charged');
-  return "(New) owed can't be > (new) charged!"
-    if $new->getfield('owed') > $new->getfield('charged');
-
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid invoice.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert and replace
-methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a cust_bill record!" unless $self->table eq "cust_bill";
-  my($recref) = $self->hashref;
-
-  $recref->{invnum} =~ /^(\d*)$/ or return "Illegal invnum";
-  $recref->{invnum} = $1;
-
-  $recref->{custnum} =~ /^(\d+)$/ or return "Illegal custnum";
-  $recref->{custnum} = $1;
-  return "Unknown customer"
-    unless qsearchs('cust_main',{'custnum'=>$recref->{custnum}});
-
-  $recref->{_date} =~ /^(\d*)$/ or return "Illegal date";
-  $recref->{_date} = $recref->{_date} ? $1 : time;
-
-  #$recref->{charged} =~ /^(\d+(\.\d\d)?)$/ or return "Illegal charged";
-  $recref->{charged} =~ /^(\-?\d+(\.\d\d)?)$/ or return "Illegal charged";
-  $recref->{charged} = $1;
-
-  $recref->{owed} =~ /^(\-?\d+(\.\d\d)?)$/ or return "Illegal owed";
-  $recref->{owed} = $1;
-
-  $recref->{printed} =~ /^(\d*)$/ or return "Illegal printed";
-  $recref->{printed} = $1 || '0';
-
-  ''; #no error
-}
-
-=item previous
-
-Returns a list consisting of the total previous balance for this customer, 
-followed by the previous outstanding invoices (as FS::cust_bill objects also).
-
-=cut
-
-sub previous {
-  my($self)=@_;
-  my($total)=0;
-  my(@cust_bill) = sort { $a->_date <=> $b->_date }
-    grep { $_->owed != 0 && $_->_date < $self->_date }
-      qsearch('cust_bill',{ 'custnum' => $self->custnum } ) 
-  ;
-  foreach (@cust_bill) { $total += $_->owed; }
-  $total, @cust_bill;
-}
-
-=item cust_bill_pkg
-
-Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
-
-=cut
-
-sub cust_bill_pkg {
-  my($self)=@_;
-  qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
-}
-
-=item cust_credit
-
-Returns a list consisting of the total previous credited (see
-L<FS::cust_credit>) for this customer, followed by the previous outstanding
-credits (FS::cust_credit objects).
-
-=cut
-
-sub cust_credit {
-  my($self)=@_;
-  my($total)=0;
-  my(@cust_credit) = sort { $a->_date <=> $b->date }
-    grep { $_->credited != 0 && $_->_date < $self->_date }
-      qsearch('cust_credit', { 'custnum' => $self->custnum } )
-  ;
-  foreach (@cust_credit) { $total += $_->credited; }
-  $total, @cust_credit;
-}
-
-=item cust_pay
-
-Returns all payments (see L<FS::cust_pay>) for this invoice.
-
-=cut
-
-sub cust_pay {
-  my($self)=@_;
-  sort { $a->_date <=> $b->date }
-    qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
-  ;
-}
-
-=item print_text [TIME];
-
-Returns an ASCII invoice, as a list of lines.
-
-TIME an optional value used to control the printing of overdue messages.  The
-default is now.  It isn't the date of the invoice; that's the `_date' field.
-It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-=cut
-
-sub print_text {
-
-  my($self,$today)=@_;
-  $today ||= time;
-  my($invnum)=$self->invnum;
-  my($cust_main) = qsearchs('cust_main', 
-                            { 'custnum', $self->custnum } );
-  $cust_main->setfield('payname',
-    $cust_main->first. ' '. $cust_main->getfield('last')
-  ) unless $cust_main->payname;
-
-  my($pr_total,@pr_cust_bill) = $self->previous; #previous balance
-  my($cr_total,@cr_cust_credit) = $self->cust_credit; #credits
-  my($balance_due) = $self->owed + $pr_total - $cr_total;
-
-  #overdue?
-  my($overdue) = ( 
-    $balance_due > 0
-    && $today > $self->_date 
-    && $self->printed > 1
-  );
-
-  #printing bits here
-
-  local($SIG{CHLD}) = sub { wait() };
-  $|=1;
-  my($pid)=open(CHILD,"-|");
-  die "Can't fork: $!" unless defined($pid); 
-
-  if ($pid) { #parent
-    my(@collect)=<CHILD>;
-    close CHILD;
-    return @collect;
-  } else { #child
-
-    my($description,$amount);
-    my(@buf);
-
-    #define format stuff
-    $%=0;
-    $= = 35;
-    local($^L) = <<END;
-
-
-
-
-
-
-
-END
-
-    #format address
-    my($l,@address)=(0,'','','','','');
-    $address[$l++]=$cust_main->company if $cust_main->company;
-    $address[$l++]=$cust_main->address1;
-    $address[$l++]=$cust_main->address2 if $cust_main->address2;
-    $address[$l++]=$cust_main->city. ", ". $cust_main->state. "  ".
-                   $cust_main->zip;
-    $address[$l++]=$cust_main->country unless $cust_main->country eq 'US';
-
-    #previous balance
-    foreach ( @pr_cust_bill ) {
-      push @buf, (
-        "Previous Balance, Invoice #". $_->invnum. 
-                   " (". time2str("%x",$_->_date). ")",
-        '$'. sprintf("%10.2f",$_->owed)
-      );
-    }
-    if (@pr_cust_bill) {
-      push @buf,('','-----------');
-      push @buf,('Total Previous Balance','$' . sprintf("%10.2f",$pr_total ) );
-      push @buf,('','');
-    }
-
-    #new charges
-    foreach ( $self->cust_bill_pkg ) {
-
-      if ( $_->pkgnum ) {
-
-        my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
-        my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
-        my($pkg)=$part_pkg->pkg;
-
-        push @buf, ( "$pkg Setup",'$' . sprintf("%10.2f",$_->setup) )
-          if $_->setup != 0;
-        push @buf, (
-          "$pkg (" . time2str("%x",$_->sdate) . " - " .
-                                time2str("%x",$_->edate) . ")",
-          '$' . sprintf("%10.2f",$_->recur)
-        ) if $_->recur != 0;
-
-      } else { #pkgnum Tax
-        push @buf,("Tax",'$' . sprintf("%10.2f",$_->setup) ) 
-          if $_->setup != 0;
-      }
-    }
-
-    push @buf,('','-----------');
-    push @buf,('Total New Charges',
-               '$' . sprintf("%10.2f",$self->charged) );
-    push @buf,('','');
-
-    push @buf,('','-----------');
-    push @buf,('Total Charges',
-               '$' . sprintf("%10.2f",$self->charged + $pr_total) );
-    push @buf,('','');
-
-    #credits
-    foreach ( @cr_cust_credit ) {
-      push @buf,(
-        "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
-        '$' . sprintf("%10.2f",$_->credited)
-      );
-    }
-
-    #get & print payments
-    foreach ( $self->cust_pay ) {
-      push @buf,(
-        "Payment received ". time2str("%x",$_->_date ),
-        '$' . sprintf("%10.2f",$_->paid )
-      );
-    }
-
-    #balance due
-    push @buf,('','-----------');
-    push @buf,('Balance Due','$' . 
-      sprintf("%10.2f",$self->owed + $pr_total - $cr_total ) );
-
-    #now print
-
-    my($tot_pages)=int(scalar(@buf)/30); #15 lines, 2 values per line
-    $tot_pages++ if scalar(@buf) % 30;
-
-    while (@buf) {
-      $description=shift(@buf);
-      $amount=shift(@buf);
-      write;
-    }
-      ($description,$amount)=('','');
-      write while ( $- );
-      print $^L;
-
-      exit; #kid
-
-    format STDOUT_TOP =
-
-                                      @|||||||||||||||||||
-                                     "Invoice"
-                                      @||||||||||||||||||| @<<<<<<< @<<<<<<<<<<<
-{
-              ( $tot_pages != 1 ) ? "Page $% of $tot_pages" : '',
-  time2str("%x",( $self->_date )), "FS-$invnum"
-}
-
-
-@>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
-$add1
-@>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
-$add2
-@>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
-$add3
-@>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
-$add4
-
-  @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
-{ $cust_main->payname,
-  ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo )
-  ? "P.O. #". $cust_main->payinfo : ''
-}
-  @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
-$address[0],''
-  @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
-$address[1],$overdue ? "* This invoice is now PAST DUE! *" : ''
-  @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
-$address[2],$overdue ? " Please forward payment promptly " : ''
-  @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
-$address[3],$overdue ? "to avoid interruption of service." : ''
-  @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<             @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
-$address[4],''
-
-
-
-.
-
-    format STDOUT =
-  @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<
-  $description,$amount
-.
-
-  } #endchild
-
-}
-
-=back
-
-=head1 BUGS
-
-The delete method.
-
-It doesn't properly override FS::Record yet.
-
-print_text formatting (and some logic :/) is in source as a format declaration,
-which needs to be slurped in from a file.  the fork is rather kludgy as well.
-It could be cleaned with swrite from man perlform, and the picture could be
-put in a /var/spool/freeside/conf file.  Also number of lines ($=).
-
-missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
-or something similar so the look can be completely customized?)
-
-There is an off-by-one error in print_text which causes a visual error: "Page 1
-of 2" printed on some single-page invoices?
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_main>, L<FS::cust_pay>, L<FS::cust_bill_pkg>,
-L<FS::cust_credit>, schema.html from the base documentation.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-1
-
-small fix for new API ivan@sisd.com 98-mar-14
-
-charges can be negative ivan@sisd.com 98-jul-13
-
-pod, ingegrate with FS::Invoice ivan@sisd.com 98-sep-20
-
-=cut
-
-1;
-
diff --git a/site_perl/cust_bill_pkg.pm b/site_perl/cust_bill_pkg.pm
deleted file mode 100644 (file)
index e41d7c1..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-package FS::cust_bill_pkg;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::Record qw(fields qsearchs);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-=head1 NAME
-
-FS::cust_bill_pkg - Object methods for cust_bill_pkg records
-
-=head1 SYNOPSIS
-
-  use FS::cust_bill_pkg;
-
-  $record = create FS::cust_bill_pkg \%hash;
-  $record = create FS::cust_bill_pkg { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::cust_bill_pkg object represents an invoice line item.
-FS::cust_bill_pkg inherits from FS::Record.  The following fields are currently
-supported:
-
-=over 4
-
-=item invnum - invoice (see L<FS::cust_bill>)
-
-=item pkgnum - package (see L<FS::cust_pkg>)
-
-=item setup - setup fee
-
-=item recur - recurring fee
-
-=item sdate - starting date of recurring fee
-
-=item edate - ending date of recurring fee
-
-=back
-
-sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">.  Also
-see L<Time::Local> and L<Date::Parse> for conversion functions.
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new line item.  To add the line item to the database, see
-L<"insert">.  Line items are normally created by calling the bill method of a
-customer object (see L<FS::cust_main>).
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('cust_bill_pkg')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('cust_bill_pkg',$hashref);
-
-}
-
-=item insert
-
-Adds this line item to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented.  I don't remove line items because there would then be
-no record the items ever existed (which is bad, no?)
-
-=cut
-
-sub delete {
-  return "Can't delete cust_bill_pkg records!";
-  #my($self)=@_;
-  #$self->del;
-}
-
-=item replace OLD_RECORD
-
-Currently unimplemented.  This would be even more of an accounting nightmare
-than deleteing the items.  Just don't do it.
-
-=cut
-
-sub replace {
-  return "Can't modify cust_bill_pkg records!";
-  #my($new,$old)=@_;
-  #return "(Old) Not a cust_bill_pkg record!" 
-  #  unless $old->table eq "cust_bill_pkg";
-  #
-  #$new->check or
-  #$new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid line item.  If there is an
-error, returns the error, otherwise returns false.  Called by the insert
-method.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a cust_bill_pkg record!" unless $self->table eq "cust_bill_pkg";
-
-  my($error)=
-    $self->ut_number('pkgnum')
-      or $self->ut_number('invnum')
-      or $self->ut_money('setup')
-      or $self->ut_money('recur')
-      or $self->ut_numbern('sdate')
-      or $self->ut_numbern('edate')
-  ;
-  return $error if $error;
-
-  if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
-    return "Unknown pkgnum ".$self->pkgnum
-    unless qsearchs('cust_pkg',{'pkgnum'=> $self->pkgnum });
-  }
-
-  return "Unknown invnum"
-    unless qsearchs('cust_bill',{'invnum'=> $self->invnum });
-
-  ''; #no error
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
-from the base documentation.
-
-=head1 HISTORY
-
-ivan@sisd.com 98-mar-13
-
-pod ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/cust_credit.pm b/site_perl/cust_credit.pm
deleted file mode 100644 (file)
index b1a5e16..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-package FS::cust_credit;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::UID qw(getotaker);
-use FS::Record qw(fields qsearchs);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-=head1 NAME
-
-FS::cust_credit - Object methods for cust_credit records
-
-=head1 SYNOPSIS
-
-  use FS::cust_credit;
-
-  $record = create FS::cust_credit \%hash;
-  $record = create FS::cust_credit { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::cust_credit object represents a credit.  FS::cust_credit inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item crednum - primary key (assigned automatically for new credits)
-
-=item custnum - customer (see L<FS::cust_main>)
-
-=item amount - amount of the credit
-
-=item credited - how much of this credit that is still outstanding, which is
-amount minus all refunds (see L<FS::cust_refund>).
-
-=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-=item otaker - order taker (assigned automatically, see L<FS::UID>)
-
-=item reason - text
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new credit.  To add the credit to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('cust_credit')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('cust_credit',$hashref);
-}
-
-=item insert
-
-Adds this credit to the database ("Posts" the credit).  If there is an error,
-returns the error, otherwise returns false.
-
-When adding new invoices, credited must be amount (or null, in which case it is
-automatically set to amount).
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->setfield('credited',$self->amount) if $self->credited eq '';
-  return "credited != amount!"
-    unless $self->credited == $self->amount;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented.
-
-=cut
-
-sub delete {
-  return "Can't remove credit!"
-  #my($self)=@_;
-  #$self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-Only credited may be changed.  Credited is normally updated by creating and
-inserting a refund (see L<FS::cust_refund>).
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a cust_credit record!" unless $old->table eq "cust_credit";
-  return "Can't change crednum!"
-    unless $old->getfield('crednum') eq $new->getfield('crednum');
-  return "Can't change custnum!"
-    unless $old->getfield('custnum') eq $new->getfield('custnum');
-  return "Can't change date!"
-    unless $old->getfield('_date') eq $new->getfield('_date');
-  return "Can't change amount!"
-    unless $old->getfield('amount') eq $new->getfield('amount');
-  return "(New) credited can't be > (new) amount!"
-    if $new->getfield('credited') > $new->getfield('amount');
-
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid credit.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert and replace
-methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a cust_credit record!" unless $self->table eq "cust_credit";
-  my($recref) = $self->hashref;
-
-  $recref->{crednum} =~ /^(\d*)$/ or return "Illegal crednum";
-  $recref->{crednum} = $1;
-
-  $recref->{custnum} =~ /^(\d+)$/ or return "Illegal custnum";
-  $recref->{custnum} = $1;
-  return "Unknown customer"
-    unless qsearchs('cust_main',{'custnum'=>$recref->{custnum}});
-
-  $recref->{_date} =~ /^(\d*)$/ or return "Illegal date";
-  $recref->{_date} = $recref->{_date} ? $1 : time;
-
-  $recref->{amount} =~ /^(\d+(\.\d\d)?)$/ or return "Illegal amount";
-  $recref->{amount} = $1;
-
-  $recref->{credited} =~ /^(\-?\d+(\.\d\d)?)$/ or return "Illegal credited";
-  $recref->{credited} = $1;
-
-  #$recref->{otaker} =~ /^(\w+)$/ or return "Illegal otaker";
-  #$recref->{otaker} = $1;
-  $self->otaker(getotaker);
-
-  $self->ut_textn('reason');
-
-}
-
-=back
-
-=head1 BUGS
-
-The delete method.
-
-It doesn't properly override FS::Record yet.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_refund>, L<FS::cust_bill>, schema.html from the base
-documentation.
-
-=head1 HISTORY
-
-ivan@sisd.com 98-mar-17
-
-pod, otaker from FS::UID ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/cust_main.pm b/site_perl/cust_main.pm
deleted file mode 100644 (file)
index ec28273..0000000
+++ /dev/null
@@ -1,868 +0,0 @@
-#this is so kludgy i'd be embarassed if it wasn't cybercash's fault
-package main;
-use vars qw($paymentserversecret $paymentserverport $paymentserverhost);
-
-package FS::cust_main;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK $conf $lpr $processor $xaction $E_NoErr);
-use Safe;
-use Exporter;
-use Carp;
-use Time::Local;
-use Date::Format;
-use Date::Manip;
-use Business::CreditCard;
-use FS::UID qw(getotaker);
-use FS::Record qw(fields hfields qsearchs qsearch);
-use FS::cust_pkg;
-use FS::cust_bill;
-use FS::cust_bill_pkg;
-use FS::cust_pay;
-#use FS::cust_pay_batch;
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(hfields);
-
-$conf = new FS::Conf;
-$lpr = $conf->config('lpr');
-
-if ( $conf->exists('cybercash3.2') ) {
-  require CCMckLib3_2;
-    #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
-  require CCMckDirectLib3_2;
-    #qw(SendCC2_1Server);
-  require CCMckErrno3_2;
-    #qw(MCKGetErrorMessage $E_NoErr);
-  import CCMckErrno3_2 qw($E_NoErr);
-  my $merchant_conf;
-  ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
-  my $status = &CCMckLib3_2::InitConfig($merchant_conf);
-  if ( $status != $E_NoErr ) {
-    warn "CCMckLib3_2::InitConfig error:\n";
-    foreach my $key (keys %CCMckLib3_2::Config) {
-      warn "  $key => $CCMckLib3_2::Config{$key}\n"
-    }
-    my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
-    die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
-  }
-  $processor='cybercash3.2';
-} elsif ( $conf->exists('cybercash2') ) {
-  require CCLib;
-    #qw(sendmserver);
-  ( $main::paymentserverhost, 
-    $main::paymentserverport, 
-    $main::paymentserversecret,
-    $xaction,
-  ) = $conf->config('cybercash2');
-  $processor='cybercash2';
-}
-
-=head1 NAME
-
-FS::cust_main - Object methods for cust_main records
-
-=head1 SYNOPSIS
-
-  use FS::cust_main;
-
-  $record = create FS::cust_main \%hash;
-  $record = create FS::cust_main { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-  @cust_pkg = $record->all_pkgs;
-
-  @cust_pkg = $record->ncancelled_pkgs;
-
-  $error = $record->bill;
-  $error = $record->bill %options;
-  $error = $record->bill 'time' => $time;
-
-  $error = $record->collect;
-  $error = $record->collect %options;
-  $error = $record->collect 'invoice_time'   => $time,
-                            'batch_card'     => 'yes',
-                            'report_badcard' => 'yes',
-                          ;
-
-=head1 DESCRIPTION
-
-An FS::cust_main object represents a customer.  FS::cust_main inherits from 
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item custnum - primary key (assigned automatically for new customers)
-
-=item agentnum - agent (see L<FS::agent>)
-
-=item refnum - referral (see L<FS::part_referral>)
-
-=item first - name
-
-=item last - name
-
-=item ss - social security number (optional)
-
-=item company - (optional)
-
-=item address1
-
-=item address2 - (optional)
-
-=item city
-
-=item county - (optional, see L<FS::cust_main_county>)
-
-=item state - (see L<FS::cust_main_county>)
-
-=item zip
-
-=item country - (see L<FS::cust_main_county>)
-
-=item daytime - phone (optional)
-
-=item night - phone (optional)
-
-=item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
-
-=item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
-
-=item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
-
-=item payname - name on card or billing name
-
-=item tax - tax exempt, empty or `Y'
-
-=item otaker - order taker (assigned automatically, see L<FS::UID>)
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new customer.  To add the customer to the database, see L<"insert">.
-
-Note that this stores the hash reference, not a distinct copy of the hash it
-points to.  You can ask the object for a copy with the I<hash> method.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my $field;
-  #foreach $field (fields('cust_main')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('cust_main',$hashref);
-}
-
-=item insert
-
-Adds this customer to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  #no callbacks in check, only data checks
-  #local $SIG{HUP} = 'IGNORE';
-  #local $SIG{INT} = 'IGNORE';
-  #local $SIG{QUIT} = 'IGNORE';
-  #local $SIG{TERM} = 'IGNORE';
-  #local $SIG{TSTP} = 'IGNORE';
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented.  Maybe cancel all of this customer's
-packages (cust_pkg)?
-
-I don't remove the customer record in the database because there would then
-be no record the customer ever existed (which is bad, no?)
-
-=cut
-
-# Usage: $error = $record -> delete;
-sub delete {
-   return "Can't (yet?) delete customers.";
-#  my($self)=@_;
-#
-#  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a cust_main record!" unless $old->table eq "cust_main";
-  return "Can't change custnum!"
-    unless $old->getfield('custnum') eq $new->getfield('custnum');
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid customer record.  If there is
-an error, returns the error, otherwise returns false.  Called by the insert
-and repalce methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-
-  return "Not a cust_main record!" unless $self->table eq "cust_main";
-
-  my $error =
-    $self->ut_number('agentnum')
-    || $self->ut_number('refnum')
-    || $self->ut_textn('company')
-    || $self->ut_text('address1')
-    || $self->ut_textn('address2')
-    || $self->ut_text('city')
-    || $self->ut_textn('county')
-    || $self->ut_text('state')
-    || $self->ut_phonen('daytime')
-    || $self->ut_phonen('night')
-    || $self->ut_phonen('fax')
-  ;
-  return $error if $error;
-
-  return "Unknown agent"
-    unless qsearchs('agent',{'agentnum'=>$self->agentnum});
-
-  return "Unknown referral"
-    unless qsearchs('part_referral',{'refnum'=>$self->refnum});
-
-  $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
-  $self->setfield('last',$1);
-
-  $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
-  $self->first($1);
-
-  if ( $self->ss eq '' ) {
-    $self->ss('');
-  } else {
-    my $ss = $self->ss;
-    $ss =~ s/\D//g;
-    $ss =~ /^(\d{3})(\d{2})(\d{4})$/
-      or return "Illegal social security number";
-    $self->ss("$1-$2-$3");
-  }
-
-  return "Unknown state/county/country"
-    unless qsearchs('cust_main_county',{
-      'state'  => $self->state,
-      'county' => $self->county,
-    } );
-
-  #int'l zips?
-  $self->zip =~ /^(\d{5}(-\d{4})?)$/ or return "Illegal zip";
-  $self->zip($1);
-
-  #int'l countries!
-  $self->country =~ /^(US)$/ or return "Illegal country";
-  $self->country($1);
-
-  $self->payby =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby";
-  $self->payby($1);
-
-  if ( $self->payby eq 'CARD' ) {
-
-    my $payinfo = $self->payinfo;
-    $payinfo =~ s/\D//g;
-    $payinfo =~ /^(\d{13,16})$/
-      or return "Illegal credit card number";
-    $payinfo = $1;
-    $self->payinfo($payinfo);
-    validate($payinfo) or return "Illegal credit card number";
-    my $type = cardtype($payinfo);
-    return "Unknown credit card type"
-      unless ( $type =~ /^VISA/ ||
-               $type =~ /^MasterCard/ ||
-               $type =~ /^American Express/ ||
-               $type =~ /^Discover/ );
-
-  } elsif ( $self->payby eq 'BILL' ) {
-
-    $self->payinfo =~ /^([\w \-]*)$/ or return "Illegal P.O. number";
-    $self->payinfo($1);
-
-  } elsif ( $self->payby eq 'COMP' ) {
-
-    $self->payinfo =~ /^(\w{2,8})$/ or return "Illegal comp account issuer";
-    $self->payinfo($1);
-
-  }
-
-  if ( $self->paydate eq '' ) {
-    return "Expriation date required" unless $self->payby eq 'BILL';
-    $self->paydate('');
-  } else {
-    $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
-      or return "Illegal expiration date";
-    if ( length($2) == 4 ) {
-      $self->paydate("$2-$1-01");
-    } elsif ( $2 > 97 ) { #should pry change to check for "this year"
-      $self->paydate("19$2-$1-01");
-    } else {
-      $self->paydate("20$2-$1-01");
-    }
-  }
-
-  if ( $self->payname eq '' ) {
-    $self->payname( $self->first. " ". $self->getfield('last') );
-  } else {
-    $self->payname =~ /^([\w \,\.\-\']+)$/
-      or return "Illegal billing name";
-    $self->payname($1);
-  }
-
-  $self->tax =~ /^(Y?)$/ or return "Illegal tax";
-  $self->tax($1);
-
-  $self->otaker(getotaker);
-
-  ''; #no error
-}
-
-=item all_pkgs
-
-Returns all packages (see L<FS::cust_pkg>) for this customer.
-
-=cut
-
-sub all_pkgs {
-  my($self)=@_;
-  qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
-}
-
-=item ncancelled_pkgs
-
-Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
-
-=cut
-
-sub ncancelled_pkgs {
-  my($self)=@_;
-  qsearch( 'cust_pkg', {
-    'custnum' => $self->custnum,
-    'cancel'  => '',
-  });
-}
-
-=item bill OPTIONS
-
-Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
-conjunction with the collect method.
-
-The only currently available option is `time', which bills the customer as if
-it were that time.  It is specified as a UNIX timestamp; see
-L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
-functions.
-
-If there is an error, returns the error, otherwise returns false.
-
-=cut
-
-sub bill {
-  my($self,%options)=@_;
-  my($time) = $options{'time'} || $^T;
-
-  my($error);
-
-  #put below somehow?
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  # find the packages which are due for billing, find out how much they are
-  # & generate invoice database.
-  my($total_setup,$total_recur)=(0,0);
-
-  my(@cust_bill_pkg);
-
-  my($cust_pkg);
-  foreach $cust_pkg (
-    qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
-  ) {
-
-    bless($cust_pkg,"FS::cust_pkg");
-    next if ( $cust_pkg->getfield('cancel') );  
-
-    #? to avoid use of uninitialized value errors... ?
-    $cust_pkg->setfield('bill', '')
-      unless defined($cust_pkg->bill);
-    my($part_pkg)=
-      qsearchs('part_pkg',{'pkgpart'=> $cust_pkg->pkgpart } );
-
-    #so we don't modify cust_pkg record unnecessarily
-    my($cust_pkg_mod_flag)=0;
-    my(%hash)=$cust_pkg->hash;
-    my($old_cust_pkg)=create FS::cust_pkg(\%hash);
-
-    # bill setup
-    my($setup)=0;
-    unless ( $cust_pkg->setup ) {
-      my($setup_prog)=$part_pkg->getfield('setup');
-      my($cpt) = new Safe;
-      #$cpt->permit(); #what is necessary?
-      $cpt->share(qw($cust_pkg)); #can $cpt now use $cust_pkg methods?
-      $setup = $cpt->reval($setup_prog);
-      unless ( defined($setup) ) {
-        warn "Error reval-ing part_pkg->setup pkgpart ", 
-             $part_pkg->pkgpart, ": $@";
-      } else {
-        $cust_pkg->setfield('setup',$time);
-        $cust_pkg_mod_flag=1; 
-      }
-    }
-
-    #bill recurring fee
-    my($recur)=0;
-    my($sdate);
-    if ( $part_pkg->getfield('freq') > 0 &&
-         ! $cust_pkg->getfield('susp') &&
-         ( $cust_pkg->getfield('bill') || 0 ) < $time
-    ) {
-      my($recur_prog)=$part_pkg->getfield('recur');
-      my($cpt) = new Safe;
-      #$cpt->permit(); #what is necessary?
-      $cpt->share(qw($cust_pkg)); #can $cpt now use $cust_pkg methods?
-      $recur = $cpt->reval($recur_prog);
-      unless ( defined($recur) ) {
-        warn "Error reval-ing part_pkg->recur pkgpart ",
-             $part_pkg->pkgpart, ": $@";
-      } else {
-        #change this bit to use Date::Manip?
-        #$sdate=$cust_pkg->bill || time;
-        #$sdate=$cust_pkg->bill || $time;
-        $sdate=$cust_pkg->bill || $cust_pkg->setup || $time;
-        my($sec,$min,$hour,$mday,$mon,$year)=
-          (localtime($sdate) )[0,1,2,3,4,5];
-        $mon += $part_pkg->getfield('freq');
-        until ( $mon < 12 ) { $mon -= 12; $year++; }
-        $cust_pkg->setfield('bill',timelocal($sec,$min,$hour,$mday,$mon,$year));
-        $cust_pkg_mod_flag=1; 
-      }
-    }
-
-    warn "setup is undefinded" unless defined($setup);
-    warn "recur is undefinded" unless defined($recur);
-    warn "cust_pkg bill is undefinded" unless defined($cust_pkg->bill);
-
-    if ($cust_pkg_mod_flag) {
-      $error=$cust_pkg->replace($old_cust_pkg);
-      if ( $error ) {
-        warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
-      } else {
-        #just in case
-        $setup=sprintf("%.2f",$setup);
-        $recur=sprintf("%.2f",$recur);
-        my($cust_bill_pkg)=create FS::cust_bill_pkg ({
-          'pkgnum' => $cust_pkg->pkgnum,
-          'setup'  => $setup,
-          'recur'  => $recur,
-          'sdate'  => $sdate,
-          'edate'  => $cust_pkg->bill,
-        });
-        push @cust_bill_pkg, $cust_bill_pkg;
-        $total_setup += $setup;
-        $total_recur += $recur;
-      }
-    }
-
-  }
-
-  my($charged)=sprintf("%.2f",$total_setup + $total_recur);
-
-  return '' if scalar(@cust_bill_pkg) == 0;
-
-  unless ( $self->getfield('tax') eq 'Y' ||
-           $self->getfield('tax') eq 'y' ||
-           $self->getfield('payby') eq 'COMP'
-  ) {
-    my($cust_main_county) = qsearchs('cust_main_county',{
-      'county' => $self->getfield('county'),
-      'state'  => $self->getfield('state'),
-    } );
-    my($tax) = sprintf("%.2f",
-      $charged * ( $cust_main_county->getfield('tax') / 100 )
-    );
-    $charged = sprintf("%.2f",$charged+$tax);
-
-    my($cust_bill_pkg)=create FS::cust_bill_pkg ({
-      'pkgnum' => 0,
-      'setup'  => $tax,
-      'recur'  => 0,
-      'sdate'  => '',
-      'edate'  => '',
-    });
-    push @cust_bill_pkg, $cust_bill_pkg;
-  }
-
-  my($cust_bill) = create FS::cust_bill ( {
-    'custnum' => $self->getfield('custnum'),
-    '_date' => $time,
-    'charged' => $charged,
-  } );
-  $error=$cust_bill->insert;
-  #shouldn't happen, but how else to handle this? (wrap me in eval, to catch 
-  # fatal errors)
-  die "Error creating cust_bill record: $error!\n",
-      "Check updated but unbilled packages for customer", $self->custnum, "\n"
-    if $error;
-
-  my($invnum)=$cust_bill->invnum;
-  my($cust_bill_pkg);
-  foreach $cust_bill_pkg ( @cust_bill_pkg ) {
-    $cust_bill_pkg->setfield('invnum',$invnum);
-    $error=$cust_bill_pkg->insert;
-    #shouldn't happen, but how else tohandle this?
-    die "Error creating cust_bill_pkg record: $error!\n",
-        "Check incomplete invoice ", $invnum, "\n"
-      if $error;
-  }
-  
-  ''; #no error
-}
-
-=item collect OPTIONS
-
-(Attempt to) collect money for this customer's outstanding invoices (see
-L<FS::cust_bill>).  Usually used after the bill method.
-
-Depending on the value of `payby', this may print an invoice (`BILL'), charge
-a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
-
-If there is an error, returns the error, otherwise returns false.
-
-Currently available options are:
-
-invoice_time - Use this time when deciding when to print invoices and
-late notices on those invoices.  The default is now.  It is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse>
-for conversion functions.
-
-batch_card - Set this true to batch cards (see L<cust_pay_batch>).  By
-default, cards are processed immediately, which will generate an error if
-CyberCash is not installed.
-
-report_badcard - Set this true if you want bad card transactions to
-return an error.  By default, they don't.
-
-=cut
-
-sub collect {
-  my($self,%options)=@_;
-  my($invoice_time) = $options{'invoice_time'} || $^T;
-
-  my($total_owed) = $self->balance;
-  return '' unless $total_owed > 0; #redundant?????
-
-  #put below somehow?
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  foreach my $cust_bill ( qsearch('cust_bill', {
-    'custnum' => $self->getfield('custnum'),
-  } ) ) {
-
-    #this has to be before next's
-    my($amount) = sprintf("%.2f", $total_owed < $cust_bill->owed
-                                  ? $total_owed
-                                  : $cust_bill->owed
-    );
-    $total_owed = sprintf("%.2f",$total_owed-$amount);
-
-    next unless $cust_bill->owed > 0;
-
-    next if qsearchs('cust_pay_batch',{'invnum'=> $cust_bill->invnum });
-
-    #warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)";
-
-    next unless $amount > 0;
-
-    if ( $self->getfield('payby') eq 'BILL' ) {
-
-      #30 days 2592000
-      my($since)=$invoice_time - ( $cust_bill->_date || 0 );
-      #warn "$invoice_time ", $cust_bill->_date, " $since";
-      if ( $since >= 0 #don't print future invoices
-           && ( $cust_bill->printed * 2592000 ) <= $since
-      ) {
-
-        open(LPR,$lpr) or die "Can't open $lpr: $!";
-        print LPR $cust_bill->print_text; #( date )
-        close LPR
-          or die $! ? "Error closing $lpr: $!"
-                       : "Exit status $? from $lpr";
-
-        my(%hash)=$cust_bill->hash;
-        $hash{'printed'}++;
-        my($new_cust_bill)=create FS::cust_bill(\%hash);
-        my($error)=$new_cust_bill->replace($cust_bill);
-        if ( $error ) {
-          warn "Error updating $cust_bill->printed: $error";
-        }
-
-      }
-
-    } elsif ( $self->getfield('payby') eq 'COMP' ) {
-      my($cust_pay) = create FS::cust_pay ( {
-         'invnum' => $cust_bill->getfield('invnum'),
-         'paid' => $amount,
-         '_date' => '',
-         'payby' => 'COMP',
-         'payinfo' => $self->getfield('payinfo'),
-         'paybatch' => ''
-      } );
-      my($error)=$cust_pay->insert;
-      return 'Error COMPing invnum #' . $cust_bill->getfield('invnum') .
-             ':' . $error if $error;
-    } elsif ( $self->getfield('payby') eq 'CARD' ) {
-
-      if ( $options{'batch_card'} ne 'yes' ) {
-
-        return "Real time card processing not enabled!" unless $processor;
-
-        if ( $processor =~ /cybercash/ ) {
-
-          #fix exp. date for cybercash
-          $self->getfield('paydate') =~ /^(\d+)\/\d*(\d{2})$/;
-          my($exp)="$1/$2";
-
-          my($paybatch)= $cust_bill->getfield('invnum') . 
-                         '-' . time2str("%y%m%d%H%M%S",time);
-
-          my($payname)= $self->getfield('payname') ||
-                        $self->getfield('first') . ' ' .$self->getfield('last');
-
-          my($address)= $self->getfield('address1');
-          $address .= ", " . $self->getfield('address2')
-            if $self->getfield('address2');
-
-          my($country) = $self->getfield('country') eq 'US' ?
-                         'USA' : $self->getfield('country');
-
-          my(@full_xaction)=($xaction,
-            'Order-ID'     => $paybatch,
-            'Amount'       => "usd $amount",
-            'Card-Number'  => $self->getfield('payinfo'),
-            'Card-Name'    => $payname,
-            'Card-Address' => $address,
-            'Card-City'    => $self->getfield('city'),
-            'Card-State'   => $self->getfield('state'),
-            'Card-Zip'     => $self->getfield('zip'),
-            'Card-Country' => $country,
-            'Card-Exp'     => $exp,
-          );
-
-          my(%result);
-          if ( $processor eq 'cybercash2' ) {
-            $^W=0; #CCLib isn't -w safe, ugh!
-            %result = &CCLib::sendmserver(@full_xaction);
-            $^W=1;
-          } elsif ( $processor eq 'cybercash3.2' ) {
-            %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
-          } else {
-            return "Unkonwn real-time processor $processor\n";
-          }
-         
-          #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
-          #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
-          if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
-            my($cust_pay) = create FS::cust_pay ( {
-               'invnum'   => $cust_bill->getfield('invnum'),
-               'paid'     => $amount,
-               '_date'     => '',
-               'payby'    => 'CARD',
-               'payinfo'  => $self->getfield('payinfo'),
-               'paybatch' => "$processor:$paybatch",
-            } );
-            my($error)=$cust_pay->insert;
-            return 'Error applying payment, invnum #' . 
-              $cust_bill->getfield('invnum') . ':' . $error if $error;
-          } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
-                 || $options{'report_badcard'} ) {
-             return 'Cybercash error, invnum #' . 
-               $cust_bill->getfield('invnum') . ':' . $result{'MErrMsg'};
-          } else {
-            return '';
-          }
-
-        } else {
-          return "Unkonwn real-time processor $processor\n";
-        }
-
-      } else { #batch card
-
-#       my($cust_pay_batch) = create FS::cust_pay_batch ( {
-       my($cust_pay_batch) = new FS::Record ('cust_pay_batch', {
-         'invnum'   => $cust_bill->getfield('invnum'),
-         'custnum'  => $self->getfield('custnum'),
-         'last'     => $self->getfield('last'),
-         'first'    => $self->getfield('first'),
-         'address1' => $self->getfield('address1'),
-         'address2' => $self->getfield('address2'),
-         'city'     => $self->getfield('city'),
-         'state'    => $self->getfield('state'),
-         'zip'      => $self->getfield('zip'),
-         'country'  => $self->getfield('country'),
-         'trancode' => 77,
-         'cardnum'  => $self->getfield('payinfo'),
-         'exp'      => $self->getfield('paydate'),
-         'payname'  => $self->getfield('payname'),
-         'amount'   => $amount,
-       } );
-#       my($error)=$cust_pay_batch->insert;
-       my($error)=$cust_pay_batch->add;
-       return "Error adding to cust_pay_batch: $error" if $error;
-
-      }
-
-    } else {
-      return "Unknown payment type ".$self->getfield('payby');
-    }
-
-  }
-  '';
-
-}
-
-=item total_owed
-
-Returns the total owed for this customer on all invoices
-(see L<FS::cust_bill>).
-
-=cut
-
-sub total_owed {
-  my($self) = @_;
-  my($total_bill) = 0;
-  my($cust_bill);
-  foreach $cust_bill ( qsearch('cust_bill', {
-    'custnum' => $self->getfield('custnum'),
-  } ) ) {
-    $total_bill += $cust_bill->getfield('owed');
-  }
-  sprintf("%.2f",$total_bill);
-}
-
-=item total_credited
-
-Returns the total credits (see L<FS::cust_credit>) for this customer.
-
-=cut
-
-sub total_credited {
-  my($self) = @_;
-  my($total_credit) = 0;
-  my($cust_credit);
-  foreach $cust_credit ( qsearch('cust_credit', {
-    'custnum' => $self->getfield('custnum'),
-  } ) ) {
-    $total_credit += $cust_credit->getfield('credited');
-  }
-  sprintf("%.2f",$total_credit);
-}
-
-=item balance
-
-Returns the balance for this customer (total owed minus total credited).
-
-=cut
-
-sub balance {
-  my($self) = @_;
-  sprintf("%.2f",$self->total_bill - $self->total_credit);
-}
-
-=back
-
-=head1 BUGS
-
-The delete method.
-
-It doesn't properly override FS::Record yet.
-
-hfields should be removed.
-
-Bill and collect options should probably be passed as references instead of a
-list.
-
-CyberCash v2 forces us to define some variables in package main.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
-L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
-L<FS::cust_main_county>, L<FS::UID>, schema.html from the base documentation.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-28
-
-Changed to standard Business::CreditCard
-no more TableUtil
-EXPORT_OK FS::Record's hfields
-removed unique calls and locking (not needed here now)
-wrapped the (now) optional fields in if statements in sub check (notyetdone!)
-ivan@sisd.com 97-nov-12
-
-updated paydate with SQL-type date info ivan@sisd.com 98-mar-5
-
-Added export of datasrc from UID.pm for Pg6.3
-changed 'day' to 'daytime' because Pg6.3 reserves the day word
-       bmccane@maxbaud.net     98-apr-3
-
-in ->create, s/svc_acct/cust_main/, now it should actually eliminate the
-warnings it was meant to ivan@sisd.com 98-jul-16
-
-don't require a phone number and allow '/' in company names
-ivan@sisd.com 98-jul-18
-
-use ut_ and rewrite &check, &*_pkgs ivan@sisd.com 98-sep-5
-
-pod, merge with FS::Bill (about time!), total_owed, total_credited and balance
-methods, cleaned collect method, source modifications no longer necessary to
-enable cybercash, cybercash v3 support, don't need to import
-FS::UID::{datasrc,checkruid} ivan@sisd.com 98-sep-19-21
-
-=cut
-
-1;
-
-
diff --git a/site_perl/cust_main_county.pm b/site_perl/cust_main_county.pm
deleted file mode 100644 (file)
index f4b4595..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-package FS::cust_main_county;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::Record qw(fields hfields qsearch qsearchs);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(hfields);
-
-=head1 NAME
-
-FS::cust_main_county - Object methods for cust_main_county objects
-
-=head1 SYNOPSIS
-
-  use FS::cust_main_county;
-
-  $record = create FS::cust_main_county \%hash;
-  $record = create FS::cust_main_county { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::cust_main_county object represents a tax rate, defined by locale.
-FS::cust_main_county inherits from FS::Record.  The following fields are
-currently supported:
-
-=over 4
-
-=item taxnum - primary key (assigned automatically for new tax rates)
-
-=item state
-
-=item county
-
-=item tax - percentage
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new tax rate.  To add the tax rate to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('cust_main_county')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('cust_main_county',$hashref);
-}
-
-=item insert
-
-Adds this tax rate to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Deletes this tax rate from the database.  If there is an error, returns the
-error, otherwise returns false.
-
-=cut
-
-sub delete {
-  my($self)=@_;
-
-  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a cust_main_county record!"
-    unless $old->table eq "cust_main_county";
-  return "Can't change taxnum!"
-    unless $old->getfield('taxnum') eq $new->getfield('taxnum');
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid tax rate.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert and replace
-methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a cust_main_county record!"
-    unless $self->table eq "cust_main_county";
-  my($recref) = $self->hashref;
-
-  $self->ut_numbern('taxnum')
-    or $self->ut_text('state')
-    or $self->ut_textn('county')
-    or $self->ut_float('tax')
-  ;
-
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-A country field (and possibly a currency field) should be added.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill>, schema.html from the base
-documentation.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-dec-16
-
-Changed check for 'tax' to use the new ut_float subroutine
-       bmccane@maxbaud.net     98-apr-3
-
-pod ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/cust_pay.pm b/site_perl/cust_pay.pm
deleted file mode 100644 (file)
index 6e30c59..0000000
+++ /dev/null
@@ -1,235 +0,0 @@
-package FS::cust_pay;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use Business::CreditCard;
-use FS::Record qw(fields qsearchs);
-use FS::cust_bill;
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-=head1 NAME
-
-FS::cust_pay - Object methods for cust_pay objects
-
-=head1 SYNOPSIS
-
-  use FS::cust_pay;
-
-  $record = create FS::cust_pay \%hash;
-  $record = create FS::cust_pay { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::cust_pay object represents a payment.  FS::cust_pay inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item paynum - primary key (assigned automatically for new payments)
-
-=item invnum - Invoice (see L<FS::cust_bill>)
-
-=item paid - Amount of this payment
-
-=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-=item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
-
-=item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
-
-=item paybatch - text field for tracking card processing
-
-=back
-
-=head1 METHODS
-
-=over 4 
-
-=item create HASHREF
-
-Creates a new payment.  To add the payment to the databse, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('cust_pay')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('cust_pay',$hashref);
-
-}
-
-=item insert
-
-Adds this payment to the databse, and updates the invoice (see
-L<FS::cust_bill>).
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  my($error);
-
-  $error=$self->check;
-  return $error if $error;
-
-  my($old_cust_bill) = qsearchs('cust_bill', {
-                                'invnum' => $self->getfield('invnum')
-                               } );
-  return "Unknown invnum" unless $old_cust_bill;
-  my(%hash)=$old_cust_bill->hash;
-  $hash{owed} = sprintf("%.2f",$hash{owed} - $self->getfield('paid') );
-  my($new_cust_bill) = create FS::cust_bill ( \%hash );
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  $error=$new_cust_bill -> replace($old_cust_bill);
-  return "Error modifying cust_bill: $error" if $error;
-
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented (accounting reasons).
-
-=cut
-
-sub delete {
-  return "Can't (yet?) delete cust_pay records!";
-#template code below
-#  my($self)=@_;
-#
-#  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Currently unimplemented (accounting reasons).
-
-=cut
-
-sub replace {
-   return "Can't (yet?) modify cust_pay records!";
-#template code below
-#  my($new,$old)=@_;
-#  return "(Old) Not a cust_pay record!" unless $old->table eq "cust_pay";
-#
-#  $new->check or
-#  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid payment.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert method.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a cust_pay record!" unless $self->table eq "cust_pay";
-  my($recref) = $self->hashref;
-
-  $recref->{paynum} =~ /^(\d*)$/ or return "Illegal paynum";
-  $recref->{paynum} = $1;
-
-  $recref->{invnum} =~ /^(\d+)$/ or return "Illegal invnum";
-  $recref->{invnum} = $1;
-
-  $recref->{paid} =~ /^(\d+(\.\d\d)?)$/ or return "Illegal paid";
-  $recref->{paid} = $1;
-
-  $recref->{_date} =~ /^(\d*)$/ or return "Illegal date";
-  $recref->{_date} = $recref->{_date} ? $1 : time;
-
-  $recref->{payby} =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby";
-  $recref->{payby} = $1;
-
-  if ( $recref->{payby} eq 'CARD' ) {
-
-    $recref->{payinfo} =~ s/\D//g;
-    if ( $recref->{payinfo} ) {
-      $recref->{payinfo} =~ /^(\d{13,16})$/
-        or return "Illegal (mistyped?) credit card number (payinfo)";
-      $recref->{payinfo} = $1;
-      #validate($recref->{payinfo})
-      #  or return "Illegal credit card number";
-      my($type)=cardtype($recref->{payinfo});
-      return "Unknown credit card type"
-        unless ( $type =~ /^VISA/ ||
-                 $type =~ /^MasterCard/ ||
-                 $type =~ /^American Express/ ||
-                 $type =~ /^Discover/ );
-    } else {
-      $recref->{payinfo}='N/A';
-    }
-
-  } elsif ( $recref->{payby} eq 'BILL' ) {
-
-    $recref->{payinfo} =~ /^([\w \-]*)$/
-      or return "Illegal P.O. number (payinfo)";
-    $recref->{payinfo} = $1;
-
-  } elsif ( $recref->{payby} eq 'COMP' ) {
-
-    $recref->{payinfo} =~ /^([\w]{2,8})$/
-      or return "Illegal comp account issuer (payinfo)";
-    $recref->{payinfo} = $1;
-
-  }
-
-  $recref->{paybatch} =~ /^([\w\-\:]*)$/
-    or return "Illegal paybatch";
-  $recref->{paybatch} = $1;
-
-  ''; #no error
-
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-Delete and replace methods.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_bill>, schema.html from the base documentation.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-1 - 25 - 29
-
-new api ivan@sisd.com 98-mar-13
-
-pod ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/cust_pkg.pm b/site_perl/cust_pkg.pm
deleted file mode 100644 (file)
index 7dc5aa7..0000000
+++ /dev/null
@@ -1,507 +0,0 @@
-package FS::cust_pkg;
-
-use strict;
-use vars qw(@ISA);
-use Exporter;
-use FS::UID qw(getotaker);
-use FS::Record qw(fields qsearch qsearchs);
-use FS::cust_svc;
-
-@ISA = qw(FS::Record Exporter);
-
-=head1 NAME
-
-FS::cust_pkg - Object methods for cust_pkg objects
-
-=head1 SYNOPSIS
-
-  use FS::cust_pkg;
-
-  $record = create FS::cust_pkg \%hash;
-  $record = create FS::cust_pkg { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-  $error = $record->cancel;
-
-  $error = $record->suspend;
-
-  $error = $record->unsuspend;
-
-  $error = FS::cust_pkg::order( $custnum, \@pkgparts );
-  $error = FS::cust_pkg::order( $custnum, \@pkgparts, \@remove_pkgnums ] );
-
-=head1 DESCRIPTION
-
-An FS::cust_pkg object represents a customer billing item.  FS::cust_pkg
-inherits from FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item pkgnum - primary key (assigned automatically for new billing items)
-
-=item custnum - Customer (see L<FS::cust_main>)
-
-=item pkgpart - Billing item definition (see L<FS::part_pkg>)
-
-=item setup - date
-
-=item bill - date
-
-=item susp - date
-
-=item expire - date
-
-=item cancel - date
-
-=item otaker - order taker (assigned automatically if null, see L<FS::UID>)
-
-=back
-
-Note: setup, bill, susp, expire and cancel are specified as UNIX timestamps;
-see L<perlfunc/"time">.  Also see L<Time::Local> and L<Date::Parse> for
-conversion functions.
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Create a new billing item.  To add the item to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('cust_pkg')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('cust_pkg',$hashref);
-}
-
-=item insert
-
-Adds this billing item to the database ("Orders" the item).  If there is an
-error, returns the error, otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented.  You don't want to delete billing items, because there
-would then be no record the customer ever purchased the item.  Instead, see
-the cancel method.
-
-sub delete {
-  return "Can't delete cust_pkg records!";
-}
-
-=item replace OLD_RECORD
-
-Replaces the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-Currently, custnum, setup, bill, susp, expire, and cancel may be changed.
-
-pkgpart may not be changed, but see the order subroutine.
-
-setup and bill are normally updated by calling the bill method of a customer
-object (see L<FS::cust_main>).
-
-suspend is normally updated by the suspend and unsuspend methods.
-
-cancel is normally updated by the cancel method (and also the order subroutine
-in some cases).
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a cust_pkg record!" if $old->table ne "cust_pkg";
-  return "Can't change pkgnum!"
-    if $old->getfield('pkgnum') ne $new->getfield('pkgnum');
-  return "Can't (yet?) change pkgpart!"
-    if $old->getfield('pkgpart') ne $new->getfield('pkgpart');
-  return "Can't change otaker!"
-    if $old->getfield('otaker') ne $new->getfield('otaker');
-  return "Can't change setup once it exists!"
-    if $old->getfield('setup') &&
-       $old->getfield('setup') != $new->getfield('setup');
-  #some logic for bill, susp, cancel?
-
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid billing item.  If there is an
-error, returns the error, otherwise returns false.  Called by the insert and
-replace methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a cust_pkg record!" if $self->table ne "cust_pkg";
-  my($recref) = $self->hashref;
-
-  $recref->{pkgnum} =~ /^(\d*)$/ or return "Illegal pkgnum";
-  $recref->{pkgnum}=$1;
-
-  $recref->{custnum} =~ /^(\d+)$/ or return "Illegal custnum";
-  $recref->{custnum}=$1;
-  return "Unknown customer"
-    unless qsearchs('cust_main',{'custnum'=>$recref->{custnum}});
-
-  $recref->{pkgpart} =~ /^(\d+)$/ or return "Illegal pkgpart";
-  $recref->{pkgpart}=$1;
-  return "Unknown pkgpart"
-    unless qsearchs('part_pkg',{'pkgpart'=>$recref->{pkgpart}});
-
-  $recref->{otaker} ||= &getotaker;
-  $recref->{otaker} =~ /^(\w{0,8})$/ or return "Illegal otaker";
-  $recref->{otaker}=$1;
-
-  $recref->{setup} =~ /^(\d*)$/ or return "Illegal setup date";
-  $recref->{setup}=$1;
-
-  $recref->{bill} =~ /^(\d*)$/ or return "Illegal bill date";
-  $recref->{bill}=$1;
-
-  $recref->{susp} =~ /^(\d*)$/ or return "Illegal susp date";
-  $recref->{susp}=$1;
-
-  $recref->{cancel} =~ /^(\d*)$/ or return "Illegal cancel date";
-  $recref->{cancel}=$1;
-
-  ''; #no error
-}
-
-=item cancel
-
-Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
-in this package, then cancels the package itself (sets the cancel field to
-now).
-
-If there is an error, returns the error, otherwise returns false.
-
-=cut
-
-sub cancel {
-  my($self)=@_;
-  my($error);
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE'; 
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  my($cust_svc);
-  foreach $cust_svc (
-    qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
-  ) {
-    my($part_svc)=
-      qsearchs('part_svc',{'svcpart'=> $cust_svc->svcpart } );
-
-    $part_svc->getfield('svcdb') =~ /^([\w\-]+)$/
-      or return "Illegal svcdb value in part_svc!";
-    my($svcdb) = $1;
-    require "FS/$svcdb.pm";
-
-    my($svc) = qsearchs($svcdb,{'svcnum' => $cust_svc->svcnum } );
-    if ($svc) {
-      bless($svc,"FS::$svcdb");
-      $error = $svc->cancel;
-      return "Error cancelling service: $error" if $error;
-      $error = $svc->delete;
-      return "Error deleting service: $error" if $error;
-    }
-
-    bless($cust_svc,"FS::cust_svc");
-    $error = $cust_svc->delete;
-    return "Error deleting cust_svc: $error" if $error;
-
-  }
-
-  unless ( $self->getfield('cancel') ) {
-    my(%hash) = $self->hash;
-    $hash{'cancel'}=$^T;
-    my($new) = create FS::cust_pkg ( \%hash );
-    $error=$new->replace($self);
-    return $error if $error;
-  }
-
-  ''; #no errors
-}
-
-=item suspend
-
-Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
-package, then suspends the package itself (sets the susp field to now).
-
-If there is an error, returns the error, otherwise returns false.
-
-=cut
-
-sub suspend {
-  my($self)=@_;
-  my($error);
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE'; 
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  my($cust_svc);
-  foreach $cust_svc (
-    qsearch('cust_svc',{'pkgnum'=> $self->getfield('pkgnum') } )
-  ) {
-    my($part_svc)=
-      qsearchs('part_svc',{'svcpart'=> $cust_svc->getfield('svcpart') } );
-
-    $part_svc->getfield('svcdb') =~ /^([\w\-]+)$/
-      or return "Illegal svcdb value in part_svc!";
-    my($svcdb) = $1;
-    require "FS/$svcdb.pm";
-
-    my($svc) = qsearchs($svcdb,{'svcnum' => $cust_svc->getfield('svcnum') } );
-
-    if ($svc) {
-      bless($svc,"FS::$svcdb");
-      $error = $svc->suspend;
-      return $error if $error;
-    }
-
-  }
-
-  unless ( $self->getfield('susp') ) {
-    my(%hash) = $self->hash;
-    $hash{'susp'}=$^T;
-    my($new) = create FS::cust_pkg ( \%hash );
-    $error=$new->replace($self);
-    return $error if $error;
-  }
-
-  ''; #no errors
-}
-
-=item unsuspend
-
-Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
-package, then unsuspends the package itself (clears the susp field).
-
-If there is an error, returns the error, otherwise returns false.
-
-=cut
-
-sub unsuspend {
-  my($self)=@_;
-  my($error);
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE'; 
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  my($cust_svc);
-  foreach $cust_svc (
-    qsearch('cust_svc',{'pkgnum'=> $self->getfield('pkgnum') } )
-  ) {
-    my($part_svc)=
-      qsearchs('part_svc',{'svcpart'=> $cust_svc->getfield('svcpart') } );
-
-    $part_svc->getfield('svcdb') =~ /^([\w\-]+)$/
-      or return "Illegal svcdb value in part_svc!";
-    my($svcdb) = $1;
-    require "FS/$svcdb.pm";
-
-    my($svc) = qsearchs($svcdb,{'svcnum' => $cust_svc->getfield('svcnum') } );
-    if ($svc) {
-      bless($svc,"FS::$svcdb");
-      $error = $svc->unsuspend;
-      return $error if $error;
-    }
-
-  }
-
-  unless ( ! $self->getfield('susp') ) {
-    my(%hash) = $self->hash;
-    $hash{'susp'}='';
-    my($new) = create FS::cust_pkg ( \%hash );
-    $error=$new->replace($self);
-    return $error if $error;
-  }
-
-  ''; #no errors
-}
-
-=back
-
-=head1 SUBROUTINES
-
-=over 4
-
-=item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF ]
-
-CUSTNUM is a customer (see L<FS::cust_main>)
-
-PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
-L<FS::part_pkg>) to order for this customer.  Duplicates are of course
-permitted.
-
-REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
-remove for this customer.  The services (see L<FS::cust_svc>) are moved to the
-new billing items.  An error is returned if this is not possible (see
-L<FS::pkg_svc>).
-
-=cut
-
-sub order {
-  my($custnum,$pkgparts,$remove_pkgnums)=@_;
-
-  my(%part_pkg);
-  # generate %part_pkg
-  # $part_pkg{$pkgpart} is true iff $custnum may purchase $pkgpart
-    my($cust_main)=qsearchs('cust_main',{'custnum'=>$custnum});
-    my($agent)=qsearchs('agent',{'agentnum'=> $cust_main->agentnum });
-
-    my($type_pkgs);
-    foreach $type_pkgs ( qsearch('type_pkgs',{'typenum'=> $agent->typenum }) ) {
-      my($pkgpart)=$type_pkgs->pkgpart;
-      $part_pkg{$pkgpart}++;
-    }
-  #
-
-  my(%svcnum);
-  # generate %svcnum
-  # for those packages being removed:
-  #@{ $svcnum{$svcpart} } goes from a svcpart to a list of FS::Record
-  # objects (table eq 'cust_svc')
-  my($pkgnum);
-  foreach $pkgnum ( @{$remove_pkgnums} ) {
-    my($cust_svc);
-    foreach $cust_svc (qsearch('cust_svc',{'pkgnum'=>$pkgnum})) {
-      push @{ $svcnum{$cust_svc->getfield('svcpart')} }, $cust_svc;
-    }
-  }
-  
-  my(@cust_svc);
-  #generate @cust_svc
-  # for those packages the customer is purchasing:
-  # @{$pkgparts} is a list of said packages, by pkgpart
-  # @cust_svc is a corresponding list of lists of FS::Record objects
-  my($pkgpart);
-  foreach $pkgpart ( @{$pkgparts} ) {
-    return "Customer not permitted to purchase pkgpart $pkgpart!"
-      unless $part_pkg{$pkgpart};
-    push @cust_svc, [
-      map {
-        ( $svcnum{$_} && @{ $svcnum{$_} } ) ? shift @{ $svcnum{$_} } : ();
-      } (split(/,/,
-       qsearchs('part_pkg',{'pkgpart'=>$pkgpart})->getfield('services')
-      ))
-    ];
-  }
-
-  #check for leftover services
-  foreach (keys %svcnum) {
-    next unless @{ $svcnum{$_} };
-    return "Leftover services!";
-  }
-
-  #no leftover services, let's make changes.
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE'; 
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE'; 
-
-  #first cancel old packages
-#  my($pkgnum);
-  foreach $pkgnum ( @{$remove_pkgnums} ) {
-    my($old) = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
-    return "Package $pkgnum not found to remove!" unless $old;
-    my(%hash) = $old->hash;
-    $hash{'cancel'}=$^T;   
-    my($new) = create FS::cust_pkg ( \%hash );
-    my($error)=$new->replace($old);
-    return $error if $error;
-  }
-
-  #now add new packages, changing cust_svc records if necessary
-#  my($pkgpart);
-  while ($pkgpart=shift @{$pkgparts} ) {
-    my($new) = create FS::cust_pkg ( {
-                                       'custnum' => $custnum,
-                                       'pkgpart' => $pkgpart,
-                                    } );
-    my($error) = $new->insert;
-    return $error if $error; 
-    my($pkgnum)=$new->getfield('pkgnum');
-    my($cust_svc);
-    foreach $cust_svc ( @{ shift @cust_svc } ) {
-      my(%hash) = $cust_svc->hash;
-      $hash{'pkgnum'}=$pkgnum;
-      my($new) = create FS::cust_svc ( \%hash );
-      my($error)=$new->replace($cust_svc);
-      return $error if $error;
-    }
-  }  
-
-  ''; #no errors
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-sub order is not OO.  Perhaps it should be moved to FS::cust_main and made so?
-
-In sub order, the @pkgparts array (passed by reference) is clobbered.
-
-Also in sub order, no money is adjusted.  Once FS::part_pkg defines a standard
-method to pass dates to the recur_prog expression, it should do so.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>
-, L<FS::pkg_svc>, schema.html from the base documentation
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-1 - 21
-
-fixed for new agent->agent_type->type_pkgs in &order ivan@sisd.com 98-mar-7
-
-pod ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/cust_refund.pm b/site_perl/cust_refund.pm
deleted file mode 100644 (file)
index a30f217..0000000
+++ /dev/null
@@ -1,233 +0,0 @@
-package FS::cust_refund;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use Business::CreditCard;
-use FS::Record qw(fields qsearchs);
-use FS::UID qw(getotaker);
-use FS::cust_credit;
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-=head1 NAME
-
-FS::cust_refund - Object method for cust_refund objects
-
-=head1 SYNOPSIS
-
-  use FS::cust_refund;
-
-  $record = create FS::cust_refund \%hash;
-  $record = create FS::cust_refund { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::cust_refund represents a refund.  FS::cust_refund inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item refundnum - primary key (assigned automatically for new refunds)
-
-=item crednum - Credit (see L<FS::cust_credit>)
-
-=item refund - Amount of the refund
-
-=item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
-L<Time::Local> and L<Date::Parse> for conversion functions.
-
-=item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
-
-=item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
-
-=item otaker - order taker (assigned automatically, see L<FS::UID>)
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new refund.  To add the refund to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('cust_refund')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('cust_refund',$hashref);
-
-}
-
-=item insert
-
-Adds this refund to the database, and updates the credit (see
-L<FS::cust_credit>).
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  my($error);
-
-  $error=$self->check;
-  return $error if $error;
-
-  my($old_cust_credit) = qsearchs('cust_credit', {
-                                'crednum' => $self->getfield('crednum')
-                               } );
-  return "Unknown crednum" unless $old_cust_credit;
-  my(%hash)=$old_cust_credit->hash;
-  $hash{credited} = sprintf("%.2f",$hash{credited} - $self->getfield('refund') );
-  my($new_cust_credit) = create FS::cust_credit ( \%hash );
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  $error=$new_cust_credit -> replace($old_cust_credit);
-  return "Error modifying cust_credit: $error" if $error;
-
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented (accounting reasons).
-
-=cut
-
-sub delete {
-  return "Can't (yet?) delete cust_refund records!";
-#template code below
-#  my($self)=@_;
-#
-#  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Currently unimplemented (accounting reasons).
-
-=cut
-
-sub replace {
-   return "Can't (yet?) modify cust_refund records!";
-#template code below
-#  my($new,$old)=@_;
-#  return "(Old) Not a cust_refund record!" unless $old->table eq "cust_refund";
-#
-#  $new->check or
-#  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid refund.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert method.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a cust_refund record!" unless $self->table eq "cust_refund";
-
-  my $error =
-    $self->ut_number('refundnum')
-    || $self->ut_number('crednum')
-    || $self->ut_money('amount')
-    || $self->ut_numbern('_date')
-  ;
-  return $error if $error;
-
-  my($recref) = $self->hashref;
-
-  $recref->{_date} ||= time;
-
-  $recref->{payby} =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby";
-  $recref->{payby} = $1;
-
-  if ( $recref->{payby} eq 'CARD' ) {
-
-    $recref->{payinfo} =~ s/\D//g;
-    if ( $recref->{payinfo} ) {
-      $recref->{payinfo} =~ /^(\d{13,16})$/
-        or return "Illegal (mistyped?) credit card number (payinfo)";
-      $recref->{payinfo} = $1;
-      #validate($recref->{payinfo})
-      #  or return "Illegal (checksum) credit card number (payinfo)";
-      my($type)=cardtype($recref->{payinfo});
-      return "Unknown credit card type"
-        unless ( $type =~ /^VISA/ ||
-                 $type =~ /^MasterCard/ ||
-                 $type =~ /^American Express/ ||
-                 $type =~ /^Discover/ );
-    } else {
-      $recref->{payinfo}='N/A';
-    }
-
-  } elsif ( $recref->{payby} eq 'BILL' ) {
-
-    $recref->{payinfo} =~ /^([\w \-]*)$/
-      or return "Illegal P.O. number (payinfo)";
-    $recref->{payinfo} = $1;
-
-  } elsif ( $recref->{payby} eq 'COMP' ) {
-
-    $recref->{payinfo} =~ /^([\w]{2,8})$/
-      or return "Illegal comp account issuer (payinfo)";
-    $recref->{payinfo} = $1;
-
-  }
-
-  $self->otaker(getotaker);
-
-  ''; #no error
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-Delete and replace methods.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_credit>, schema.html from the base documentation.
-
-=head1 HISTORY
-
-ivan@sisd.com 98-mar-18
-
-->create had wrong tablename ivan@sisd.com 98-jun-16
-(finish me!)
-
-pod and finish up ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/cust_svc.pm b/site_perl/cust_svc.pm
deleted file mode 100644 (file)
index 1d5051b..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-package FS::cust_svc;
-
-use strict;
-use vars qw(@ISA);
-use Exporter;
-use FS::Record qw(fields qsearchs);
-
-@ISA = qw(FS::Record Exporter);
-
-=head1 NAME
-
-FS::cust_svc - Object method for cust_svc objects
-
-=head1 SYNOPSIS
-
-  use FS::cust_svc;
-
-  $record = create FS::cust_svc \%hash
-  $record = create FS::cust_svc { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::cust_svc represents a service.  FS::cust_svc inherits from FS::Record.
-The following fields are currently supported:
-
-=over 4
-
-=item svcnum - primary key (assigned automatically for new services)
-
-=item pkgnum - Package (see L<FS::cust_pkg>)
-
-=item svcpart - Service definition (see L<FS::part_svc>)
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new service.  To add the refund to the database, see L<"insert">.
-Services are normally created by creating FS::svc_ objects (see
-L<FS::svc_acct>, L<FS::svc_domain>, and L<FS::svc_acct_sm>, among others).
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_; 
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('cust_svc')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('cust_svc',$hashref);
-}
-
-=item insert
-
-Adds this service to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Deletes this service from the database.  If there is an error, returns the
-error, otherwise returns false.
-
-Called by the cancel method of the package (see L<FS::cust_pkg>).
-
-=cut
-
-sub delete {
-  my($self)=@_;
-  # anything else here?
-  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a cust_svc record!" unless $old->table eq "cust_svc";
-  return "Can't change svcnum!"
-    unless $old->getfield('svcnum') eq $new->getfield('svcnum');
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid service.  If there is an error,
-returns the error, otehrwise returns false.  Called by the insert and
-replace methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a cust_svc record!" unless $self->table eq "cust_svc";
-  my($recref) = $self->hashref;
-
-  $recref->{svcnum} =~ /^(\d*)$/ or return "Illegal svcnum";
-  $recref->{svcnum}=$1;
-
-  $recref->{pkgnum} =~ /^(\d*)$/ or return "Illegal pkgnum";
-  $recref->{pkgnum}=$1;
-  return "Unknown pkgnum" unless
-    ! $recref->{pkgnum} ||
-    qsearchs('cust_pkg',{'pkgnum'=>$recref->{pkgnum}});
-
-  $recref->{svcpart} =~ /^(\d+)$/ or return "Illegal svcpart";
-  $recref->{svcpart}=$1;
-  return "Unknown svcpart" unless
-    qsearchs('part_svc',{'svcpart'=>$recref->{svcpart}});
-
-  ''; #no error
-}
-
-=back
-
-=head1 BUGS
-
-Behaviour of changing the svcpart of cust_svc records is undefined and should
-possibly be prohibited, and pkg_svc records are not checked.
-
-pkg_svc records are not checket in general (here).
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_pkg>, L<FS::part_svc>, L<FS::pkg_svc>, 
-schema.html from the base documentation
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-10,14
-
-no TableUtil, no FS::Lock ivan@sisd.com 98-mar-7
-
-pod ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/dbdef.pm b/site_perl/dbdef.pm
deleted file mode 100644 (file)
index ac31bff..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-package FS::dbdef;
-
-use strict;
-use vars qw(@ISA);
-use Exporter;
-use Carp;
-use FreezeThaw qw(freeze thaw cmpStr);
-use FS::dbdef_table;
-use FS::dbdef_unique;
-use FS::dbdef_index;
-use FS::dbdef_column;
-
-@ISA = qw(Exporter);
-
-=head1 NAME
-
-FS::dbdef - Database objects
-
-=head1 SYNOPSIS
-
-  use FS::dbdef;
-
-  $dbdef = new FS::dbdef (@dbdef_table_objects);
-  $dbdef = load FS::dbdef "filename";
-
-  $dbdef->save("filename");
-
-  $dbdef->addtable($dbdef_table_object);
-
-  @table_names = $dbdef->tables;
-
-  $FS_dbdef_table_object = $dbdef->table;
-
-=head1 DESCRIPTION
-
-FS::dbdef objects are collections of FS::dbdef_table objects and represnt
-a database (a collection of tables).
-
-=head1 METHODS
-
-=over 4
-
-=item new TABLE, TABLE, ...
-
-Creates a new FS::dbdef object
-
-=cut
-
-sub new {
-  my($proto,@tables)=@_;
-  my(%tables)=map  { $_->name, $_ } @tables; #check for duplicates?
-
-  my($class) = ref($proto) || $proto;
-  my($self) = {
-    'tables' => \%tables,
-  };
-
-  bless ($self, $class);
-
-}
-
-=item load FILENAME
-
-Loads an FS::dbdef object from a file.
-
-=cut
-
-sub load {
-  my($proto,$file)=@_; #use $proto ?
-  open(FILE,"<$file") or die "Can't open $file: $!";
-  my($string)=join('',<FILE>); #can $string have newlines?  pry not?
-  close FILE or die "Can't close $file: $!";
-  my($self)=thaw $string;
-  #no bless needed?
-  $self;
-}
-
-=item save FILENAME
-
-Saves an FS::dbdef object to a file.
-
-=cut
-
-sub save {
-  my($self,$file)=@_;
-  my($string)=freeze $self;
-  open(FILE,">$file") or die "Can't open $file: $!";
-  print FILE $string;
-  close FILE or die "Can't close file: $!";
-  my($check_self)=thaw $string;
-  die "Verify error: Can't freeze and thaw dbdef $self"
-    if (cmpStr($self,$check_self));
-}
-
-=item addtable TABLE
-
-Adds this FS::dbdef_table object.
-
-=cut
-
-sub addtable {
-  my($self,$table)=@_;
-  ${$self->{'tables'}}{$table->name}=$table; #check for dupliates?
-}
-
-=item tables 
-
-Returns the names of all tables.
-
-=cut
-
-sub tables {
-  my($self)=@_;
-  keys %{$self->{'tables'}};
-}
-
-=item table TABLENAME
-
-Returns the named FS::dbdef_table object.
-
-=cut
-
-sub table {
-  my($self,$table)=@_;
-  $self->{'tables'}->{$table};
-}
-
-=head1 BUGS
-
-Each FS::dbdef object should have a name which corresponds to its name within
-the SQL database engine.
-
-=head1 SEE ALSO
-
-L<FS::dbdef_table>, L<FS::Record>,
-
-=head1 HISTORY
-
-beginning of abstraction into a class (not really)
-
-ivan@sisd.com 97-dec-4
-
-added primary_key
-ivan@sisd.com 98-jan-20
-
-added datatype (very kludgy and needs to be cleaned)
-ivan@sisd.com 98-feb-21
-
-perltrap (sigh) masked by mysql 3.20->3,21 ivan@sisd.com 98-mar-2
-
-Change 'type' to 'atype' in agent_type
-Changed attributes to special words which are changed in fs-setup
-       ie. double(10,2) <=> MONEYTYPE
-Changed order of some of the field definitions because Pg6.3 is picky
-Changed 'day' to 'daytime' in cust_main
-Changed type of tax from tinyint to real
-Change 'password' to '_password' in svc_acct
-Pg6.3 does not allow 'field char(x) NULL'
-       bmccane@maxbaud.net     98-apr-3
-
-rewrite: now properly OO.  See also FS::dbdef_{table,column,unique,index}
-
-ivan@sisd.com 98-apr-17
-
-gained some extra functions ivan@sisd.com 98-may-11
-
-now knows how to Freeze and Thaw itself ivan@sisd.com 98-jun-2
-
-pod ivan@sisd.com 98-sep-23
-
-=cut
-
-1;
-
diff --git a/site_perl/dbdef_colgroup.pm b/site_perl/dbdef_colgroup.pm
deleted file mode 100644 (file)
index 64f2e30..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-package FS::dbdef_colgroup;
-
-use strict;
-use vars qw(@ISA);
-
-@ISA = qw(Exporter);
-
-=head1 NAME
-
-FS::dbdef_colgroup - Column group objects
-
-=head1 SYNOPSIS
-
-  use FS::dbdef_colgroup;
-
-  $colgroup = new FS::dbdef_colgroup ( $lol );
-  $colgroup = new FS::dbdef_colgroup (
-    [
-      [ 'single_column' ],
-      [ 'multiple_columns', 'another_column', ],
-    ]
-  );
-
-  @sql_lists = $colgroup->sql_list;
-
-  @singles = $colgroup->singles;
-
-=head1 DESCRIPTION
-
-FS::dbdef_colgroup objects represent sets of sets of columns.
-
-=head1 METHODS
-
-=over 4
-
-=item new
-
-Creates a new FS::dbdef_colgroup object.
-
-=cut
-
-sub new {
-  my($proto, $lol) = @_;
-
-  my $class = ref($proto) || $proto;
-  my $self = {
-    'lol' => $lol,
-  };
-
-  bless ($self, $class);
-
-}
-
-=item sql_list
-
-Returns a flat list of comma-separated values, for SQL statements.
-
-=cut
-
-sub sql_list { #returns a flat list of comman-separates lists (for sql)
-  my($self)=@_;
-   grep $_ ne '', map join(', ', @{$_}), @{$self->{'lol'}};
-}
-
-=item singles
-
-Returns a flat list of all single item lists.
-
-=cut
-
-sub singles { #returns single-field groups as a flat list
-  my($self)=@_;
-  #map ${$_}[0], grep scalar(@{$_}) == 1, @{$self->{'lol'}};
-  map { 
-    ${$_}[0] =~ /^(\w+)$/
-      #aah!
-      or die "Illegal column ", ${$_}[0], " in colgroup!";
-    $1;
-  } grep scalar(@{$_}) == 1, @{$self->{'lol'}};
-}
-
-=back
-
-=head1 BUGS
-
-=head1 SEE ALSO
-
-L<FS::dbdef_table>, L<FS::dbdef_unique>, L<FS::dbdef_index>,
-L<FS::dbdef_column>, L<FS::dbdef>, L<perldsc>
-
-=head1 HISTORY
-
-class for dealing with groups of groups of columns (used as a base class by
-FS::dbdef_{unique,index} )
-
-ivan@sisd.com 98-apr-19
-
-added singles, fixed sql_list to skip empty lists ivan@sisd.com 98-jun-2
-
-untaint things we're returning in sub singels ivan@sisd.com 98-jun-4
-
-pod ivan@sisd.com 98-sep-24
-
-=cut
-
-1;
-
diff --git a/site_perl/dbdef_column.pm b/site_perl/dbdef_column.pm
deleted file mode 100644 (file)
index 023b57d..0000000
+++ /dev/null
@@ -1,175 +0,0 @@
-package FS::dbdef_column;
-
-use strict;
-#use Carp;
-use Exporter;
-use vars qw(@ISA);
-
-@ISA = qw(Exporter);
-
-=head1 NAME
-
-FS::dbdef_column - Column object
-
-=head1 SYNOPSIS
-
-  use FS::dbdef_column;
-
-  $column_object = new FS::dbdef_column ( $name, $sql_type, '' );
-  $column_object = new FS::dbdef_column ( $name, $sql_type, 'NULL' );
-  $column_object = new FS::dbdef_column ( $name, $sql_type, '', $length );
-  $column_object = new FS::dbdef_column ( $name, $sql_type, 'NULL', $length );
-
-  $name = $column_object->name;
-  $column_object->name ( 'name' );
-
-  $name = $column_object->type;
-  $column_object->name ( 'sql_type' );
-
-  $name = $column_object->null;
-  $column_object->name ( 'NOT NULL' );
-
-  $name = $column_object->length;
-  $column_object->name ( $length );
-
-  $sql_line = $column->line;
-  $sql_line = $column->line $datasrc;
-
-=head1 DESCRIPTION
-
-FS::dbdef::column objects represend columns in tables (see L<FS::dbdef_table>).
-
-=head1 METHODS
-
-=over 4
-
-=item new
-
-Creates a new FS::dbdef_column object.
-
-=cut
-
-sub new {
-  my($proto,$name,$type,$null,$length)=@_;
-
-  #croak "Illegal name: $name" if grep $name eq $_, @reserved_words;
-
-  $null =~ s/^NOT NULL$//i;
-
-  my $class = ref($proto) || $proto;
-  my $self = {
-    'name'   => $name,
-    'type'   => $type,
-    'null'   => $null,
-    'length' => $length,
-  };
-
-  bless ($self, $class);
-
-}
-
-=item name
-
-Returns or sets the column name.
-
-=cut
-
-sub name {
-  my($self,$value)=@_;
-  if ( defined($value) ) {
-  #croak "Illegal name: $name" if grep $name eq $_, @reserved_words;
-    $self->{'name'} = $value;
-  } else {
-    $self->{'name'};
-  }
-}
-
-=item type
-
-Returns or sets the column type.
-
-=cut
-
-sub type {
-  my($self,$value)=@_;
-  if ( defined($value) ) {
-    $self->{'type'} = $value;
-  } else {
-    $self->{'type'};
-  }
-}
-
-=item null
-
-Returns or sets the column null flag.
-
-=cut
-
-sub null {
-  my($self,$value)=@_;
-  if ( defined($value) ) {
-    $value =~ s/^NOT NULL$//i;
-    $self->{'null'} = $value;
-  } else {
-    $self->{'null'};
-  }
-}
-
-=item type
-
-Returns or sets the column length.
-
-=cut
-
-sub length {
-  my($self,$value)=@_;
-  if ( defined($value) ) {
-    $self->{'length'} = $value;
-  } else {
-    $self->{'length'};
-  }
-}
-
-=item line [ $datasrc ]
-
-Returns an SQL column definition.
-
-If passed a DBI $datasrc specifying L<DBD::mysql>, will use MySQL-specific
-syntax.  Non-standard syntax for other engines (if applicable) may also be
-supported in the future.
-
-=cut
-
-sub line {
-  my($self,$datasrc)=@_;
-  my($null)=$self->null;
-  $null ||= "NOT NULL" if $datasrc =~ /mysql/; #yucky mysql hack
-  join(' ',
-    $self->name,
-    $self->type. ( $self->length ? '('.$self->length.')' : '' ),
-    $null,
-  );
-}
-
-=back
-
-=head1 BUGS
-
-=head1 SEE ALSO
-
-L<FS::dbdef_table>, L<FS::dbdef>, L<DBI>
-
-=head1 HISTORY
-
-class for dealing with column definitions
-
-ivan@sisd.com 98-apr-17
-
-now methods can be used to get or set data ivan@sisd.com 98-may-11
-
-mySQL-specific hack for null (what should be default?) ivan@sisd.com 98-jun-2
-
-=cut
-
-1;
-
diff --git a/site_perl/dbdef_index.pm b/site_perl/dbdef_index.pm
deleted file mode 100644 (file)
index 2097db1..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-package FS::dbdef_index;
-
-use strict;
-use vars qw(@ISA);
-use FS::dbdef_colgroup;
-
-@ISA=qw(FS::dbdef_colgroup);
-
-=head1 NAME
-
-FS::dbdef_unique.pm - Index object
-
-=head1 SYNOPSIS
-
-  use FS::dbdef_index;
-
-    # see FS::dbdef_colgroup methods
-
-=head1 DESCRIPTION
-
-FS::dbdef_unique objects represent the (non-unique) indices of a table
-(L<FS::dbdef_table>).  FS::dbdef_unique inherits from FS::dbdef_colgroup.
-
-=head1 BUGS
-
-Is this empty subclass needed?
-
-=head1 SEE ALSO
-
-L<FS::dbdef_colgroup>, L<FS::dbdef_record>, L<FS::Record>
-
-=head1 HISTORY
-
-class for dealing with index definitions
-
-ivan@sisd.com 98-apr-19
-
-pod ivan@sisd.com 98-sep-24
-
-=cut
-
-1;
-
diff --git a/site_perl/dbdef_table.pm b/site_perl/dbdef_table.pm
deleted file mode 100644 (file)
index bc1454d..0000000
+++ /dev/null
@@ -1,249 +0,0 @@
-package FS::dbdef_table;
-
-use strict;
-#use Carp;
-use Exporter;
-use vars qw(@ISA);
-use FS::dbdef_column;
-
-@ISA = qw(Exporter);
-
-=head1 NAME
-
-FS::dbdef_table - Table objects
-
-=head1 SYNOPSIS
-
-  use FS::dbdef_table;
-
-  $dbdef_table = new FS::dbdef_table (
-    "table_name",
-    "primary_key",
-    $FS_dbdef_unique_object,
-    $FS_dbdef_index_object,
-    @FS_dbdef_column_objects,
-  );
-
-  $dbdef_table->addcolumn ( $FS_dbdef_column_object );
-
-  $table_name = $dbdef_table->name;
-  $dbdef_table->name ("table_name");
-
-  $table_name = $dbdef_table->primary_keye;
-  $dbdef_table->primary_key ("primary_key");
-
-  $FS_dbdef_unique_object = $dbdef_table->unique;
-  $dbdef_table->unique ( $FS_dbdef_unique_object );
-
-  $FS_dbdef_index_object = $dbdef_table->index;
-  $dbdef_table->index ( $FS_dbdef_index_object );
-
-  @column_names = $dbdef->columns;
-
-  $FS_dbdef_column_object = $dbdef->column;
-
-  @sql_statements = $dbdef->sql_create_table;
-  @sql_statements = $dbdef->sql_create_table $datasrc;
-
-=head1 DESCRIPTION
-
-FS::dbdef_table objects represent a single database table.
-
-=head1 METHODS
-
-=over 4
-
-=item new
-
-Creates a new FS::dbdef_table object.
-
-=cut
-
-sub new {
-  my($proto,$name,$primary_key,$unique,$index,@columns)=@_;
-
-  my(%columns) = map { $_->name, $_ } @columns;
-
-  #check $primary_key, $unique and $index to make sure they are $columns ?
-  # (and sanity check?)
-
-  my $class = ref($proto) || $proto;
-  my $self = {
-    'name'        => $name,
-    'primary_key' => $primary_key,
-    'unique'      => $unique,
-    'index'       => $index,
-    'columns'     => \%columns,
-  };
-
-  bless ($self, $class);
-
-}
-
-=item addcolumn
-
-Adds this FS::dbdef_column object. 
-
-=cut
-
-sub addcolumn {
-  my($self,$column)=@_;
-  ${$self->{'columns'}}{$column->name}=$column; #sanity check?
-}
-
-=item name
-
-Returns or sets the table name.
-
-=cut
-
-sub name {
-  my($self,$value)=@_;
-  if ( defined($value) ) {
-    $self->{name} = $value;
-  } else {
-    $self->{name};
-  }
-}
-
-=item primary_key
-
-Returns or sets the primary key.
-
-=cut
-
-sub primary_key {
-  my($self,$value)=@_;
-  if ( defined($value) ) {
-    $self->{primary_key} = $value;
-  } else {
-    #$self->{primary_key};
-    #hmm.  maybe should untaint the entire structure when it comes off disk 
-    # cause if you don't trust that, ?
-    $self->{primary_key} =~ /^(\w*)$/ 
-      #aah!
-      or die "Illegal primary key ", $self->{primary_key}, " in dbdef!\n";
-    $1;
-  }
-}
-
-=item unique
-
-Returns or sets the FS::dbdef_unique object.
-
-=cut
-
-sub unique { 
-  my($self,$value)=@_;
-  if ( defined($value) ) {
-    $self->{unique} = $value;
-  } else {
-    $self->{unique};
-  }
-}
-
-=item index
-
-Returns or sets the FS::dbdef_index object.
-
-=cut
-
-sub index { 
-  my($self,$value)=@_;
-  if ( defined($value) ) {
-    $self->{'index'} = $value;
-  } else {
-    $self->{'index'};
-  }
-}
-
-=item columns
-
-Returns a list consisting of the names of all columns.
-
-=cut
-
-sub columns {
-  my($self)=@_;
-  keys %{$self->{'columns'}};
-}
-
-=item column "column"
-
-Returns the column object (see L<FS::dbdef_column>) for "column".
-
-=cut
-
-sub column {
-  my($self,$column)=@_;
-  $self->{'columns'}->{$column};
-}
-
-=item sql_create_table [ $datasrc ]
-
-Returns an array of SQL statments to create this table.
-
-If passed a DBI $datasrc specifying L<DBD::mysql>, will use MySQL-specific
-syntax.  Non-standard syntax for other engines (if applicable) may also be
-supported in the future.
-
-=cut
-
-sub sql_create_table { 
-  my($self,$datasrc)=@_;
-
-  my(@columns)=map { $self->column($_)->line($datasrc) } $self->columns;
-  push @columns, "PRIMARY KEY (". $self->primary_key. ")"
-    if $self->primary_key;
-  if ( $datasrc =~ /mysql/ ) { #yucky mysql hack
-    push @columns, map "UNIQUE ($_)", $self->unique->sql_list;
-    push @columns, map "INDEX ($_)", $self->index->sql_list;
-  }
-
-  "CREATE TABLE ". $self->name. " ( ". join(", ", @columns). " )",
-  ( map {
-    my($index) = $_ . "_index";
-    $index =~ s/,\s*/_/g;
-    "CREATE UNIQUE INDEX $index ON ". $self->name. " ($_)"
-  } $self->unique->sql_list ),
-  ( map {
-    my($index) = $_ . "_index";
-    $index =~ s/,\s*/_/g;
-    "CREATE INDEX $index ON ". $self->name. " ($_)"
-  } $self->index->sql_list ),
-  ;  
-
-
-}
-
-=back
-
-=head1 BUGS
-
-=head1 SEE ALSO
-
-L<FS::dbdef>, L<FS::dbdef_unique>, L<FS::dbdef_index>, L<FS::dbdef_unique>,
-L<DBI>
-
-=head1 HISTORY
-
-class for dealing with table definitions
-
-ivan@sisd.com 98-apr-18
-
-gained extra functions (should %columns be an IxHash?)
-ivan@sisd.com 98-may-11
-
-sql_create_table returns a list of statments, not just one, and now it
-does indices (plus mysql hack) ivan@sisd.com 98-jun-2
-
-untaint primary_key... hmm.  is this a hack around a bigger problem?
-looks like, did the same thing singles in colgroup!
-ivan@sisd.com 98-jun-4
-
-pod ivan@sisd.com 98-sep-24
-
-=cut
-
-1;
-
diff --git a/site_perl/dbdef_unique.pm b/site_perl/dbdef_unique.pm
deleted file mode 100644 (file)
index 4ec40de..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-package FS::dbdef_unique;
-
-use strict;
-use vars qw(@ISA);
-use FS::dbdef_colgroup;
-
-@ISA=qw(FS::dbdef_colgroup);
-
-=head1 NAME
-
-FS::dbdef_unique.pm - Unique object
-
-=head1 SYNOPSIS
-
-  use FS::dbdef_unique;
-
-  # see FS::dbdef_colgroup methods
-
-=head1 DESCRIPTION
-
-FS::dbdef_unique objects represent the unique indices of a database table
-(L<FS::dbdef_table>).  FS::dbdef_unique inherits from FS::dbdef_colgroup.
-
-=head1 BUGS
-
-Is this empty subclass needed?
-
-=head1 SEE ALSO
-
-L<FS::dbdef_colgroup>, L<FS::dbdef_record>, L<FS::Record>
-
-=head1 HISTORY
-
-class for dealing with unique definitions
-
-ivan@sisd.com 98-apr-19
-
-pod ivan@sisd.com 98-sep-24
-
-=cut
-
-1;
-
-
diff --git a/site_perl/part_pkg.pm b/site_perl/part_pkg.pm
deleted file mode 100644 (file)
index d1c12e4..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-package FS::part_pkg;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::Record qw(fields hfields);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(hfields fields);
-
-=head1 NAME
-
-FS::part_pkg - Object methods for part_pkg objects
-
-=head1 SYNOPSIS
-
-  use FS::part_pkg;
-
-  $record = create FS::part_pkg \%hash
-  $record = create FS::part_pkg { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::part_pkg represents a billing item definition.  FS::part_pkg inherits
-from FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item pkgpart - primary key (assigned automatically for new billing item definitions)
-
-=item pkg - Text name of this billing item definition (customer-viewable)
-
-=item comment - Text name of this billing item definition (non-customer-viewable)
-
-=item setup - Setup fee
-
-=item freq - Frequency of recurring fee
-
-=item recur - Recurring fee
-
-=back
-
-setup and recur are evaluated as Safe perl expressions.  You can use numbers
-just as you would normally.  More advanced semantics are not yet defined.
-
-=head1 METHODS
-
-=over 4 
-
-=item create HASHREF
-
-Creates a new billing item definition.  To add the billing item definition to
-the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('part_pkg')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('part_pkg',$hashref);
-}
-
-=item insert
-
-Adds this billing item definition to the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented.
-
-=cut
-
-sub delete {
-  return "Can't (yet?) delete package definitions.";
-# maybe check & make sure the pkgpart isn't in cust_pkg or type_pkgs?
-#  my($self)=@_;
-#
-#  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a part_pkg record!" unless $old->table eq "part_pkg";
-  return "Can't change pkgpart!"
-    unless $old->getfield('pkgpart') eq $new->getfield('pkgpart');
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid billing item definition.  If
-there is an error, returns the error, otherwise returns false.  Called by the
-insert and replace methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a part_pkg record!" unless $self->table eq "part_pkg";
-
-  $self->ut_numbern('pkgpart')
-    or $self->ut_text('pkg')
-    or $self->ut_text('comment')
-    or $self->ut_anything('setup')
-    or $self->ut_number('freq')
-    or $self->ut_anything('recur')
-  ;
-
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-The delete method is unimplemented.
-
-setup and recur semantics are not yet defined (and are implemented in
-FS::cust_bill.  hmm.).
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_pkg>, L<FS::type_pkgs>, L<FS::pkg_svc>, L<Safe>.
-schema.html from the base documentation.
-
-=head1 HISTORY
-
-ivan@sisd.com 97-dec-5
-
-pod ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/part_referral.pm b/site_perl/part_referral.pm
deleted file mode 100644 (file)
index 1b4a1b6..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-package FS::part_referral;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::Record qw(fields qsearchs);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-=head1 NAME
-
-FS::part_referral - Object methods for part_referral objects
-
-=head1 SYNOPSIS
-
-  use FS::part_referral;
-
-  $record = create FS::part_referral \%hash
-  $record = create FS::part_referral { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::part_referral represents a referral - where a customer heard of your
-services.  This can be used to track the effectiveness of a particular piece of
-advertising, for example.  FS::part_referral inherits from FS::Record.  The
-following fields are currently supported:
-
-=over 4
-
-=item refnum - primary key (assigned automatically for new referrals)
-
-=item referral - Text name of this referral
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new referral.  To add the referral to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('part_referral')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('part_referral',$hashref);
-}
-
-=item insert
-
-Adds this referral to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented.
-
-=cut
-
-sub delete {
-  my($self)=@_;
-  return "Can't (yet?) delete part_referral records";
-  #$self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not an part_referral record!" 
-    unless $old->table eq "part_referral";
-  return "Can't change refnum!"
-    unless $old->getfield('refnum') eq $new->getfield('refnum');
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid referral.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert and replace
-methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a part_referral record!" unless $self->table eq "part_referral";
-
-  my($error)=
-    $self->ut_numbern('refnum')
-      or $self->ut_text('referral')
-  ;
-  return $error if $error;
-
-  '';
-
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-The delete method is unimplemented.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::cust_main>, schema.html from the base documentation.
-
-=head1 HISTORY
-
-Class dealing with referrals
-
-ivan@sisd.com 98-feb-23
-
-pod ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/part_svc.pm b/site_perl/part_svc.pm
deleted file mode 100644 (file)
index 0fd8ee4..0000000
+++ /dev/null
@@ -1,199 +0,0 @@
-package FS::part_svc;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::Record qw(fields hfields);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(hfields fields);
-
-=head1 NAME
-
-FS::part_svc - Object methods for part_svc objects
-
-=head1 SYNOPSIS
-
-  use FS::part_svc;
-
-  $record = create FS::part_referral \%hash
-  $record = create FS::part_referral { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::part_svc represents a service definition.  FS::part_svc inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item svcpart - primary key (assigned automatically for new service definitions)
-
-=item svc - text name of this service definition
-
-=item svcdb - table used for this service.  See L<FS::svc_acct>,
-L<FS::svc_domain>, and L<FS::svc_acct_sm>, among others.
-
-=item I<svcdb>__I<field> - Default or fixed value for I<field> in I<svcdb>.
-
-=item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null, `D' for default, or `F' for fixed
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new service definition.  To add the service definition to the
-database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('part_svc')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('part_svc',$hashref);
-}
-
-=item insert
-
-Adds this service definition to the database.  If there is an error, returns
-the error, otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented.
-
-=cut
-
-sub delete {
-  return "Can't (yet?) delete service definitions.";
-# maybe check & make sure the svcpart isn't in cust_svc or (in any packages)?
-#  my($self)=@_;
-#
-#  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a part_svc record!" unless $old->table eq "part_svc";
-  return "Can't change svcpart!"
-    unless $old->getfield('svcpart') eq $new->getfield('svcpart');
-  return "Can't change svcdb!"
-    unless $old->getfield('svcdb') eq $new->getfield('svcdb');
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid service definition.  If there is
-an error, returns the error, otherwise returns false.  Called by the insert
-and replace methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a part_svc record!" unless $self->table eq "part_svc";
-  my($recref) = $self->hashref;
-
-  my($error);
-  return $error if $error=
-    $self->ut_numbern('svcpart')
-    || $self->ut_text('svc')
-    || $self->ut_alpha('svcdb')
-  ;
-
-  my(@fields) = eval { fields($recref->{svcdb}) }; #might die
-  return "Unknown svcdb!" unless @fields;
-
-  my($svcdb);
-  foreach $svcdb ( qw(
-    svc_acct svc_acct_sm svc_charge svc_domain svc_wo
-  ) ) {
-    my(@rows)=map { /^${svcdb}__(.*)$/; $1 }
-      grep ! /_flag$/,
-        grep /^${svcdb}__/,
-          fields('part_svc');
-    my($row);
-    foreach $row (@rows) {
-      unless ( $svcdb eq $recref->{svcdb} ) {
-        $recref->{$svcdb.'__'.$row}='';
-        $recref->{$svcdb.'__'.$row.'_flag'}='';
-        next;
-      }
-      $recref->{$svcdb.'__'.$row.'_flag'} =~ /^([DF]?)$/
-        or return "Illegal flag for $svcdb $row";
-      $recref->{$svcdb.'__'.$row.'_flag'} = $1;
-
-#      $recref->{$svcdb.'__'.$row} =~ /^(.*)$/ #not restrictive enough?
-#        or return "Illegal value for $svcdb $row";
-#      $recref->{$svcdb.'__'.$row} = $1;
-      my($error);
-      return $error if $error=$self->ut_anything($svcdb.'__'.$row);
-
-    }
-  }
-
-  ''; #no error
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-Delete is unimplemented.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::part_pkg>, L<FS::pkg_svc>, L<FS::cust_svc>,
-L<FS::svc_acct>, L<FS::svc_acct_sm>, L<FS::svc_domain>, schema.html from the
-base documentation.
-
-=head1 HISTORY
-
-ivan@sisd.com 97-nov-14
-
-data checking/untainting calls into FS::Record added
-ivan@sisd.com 97-dec-6
-
-pod ivan@sisd.com 98-sep-21
-
-=cut
-
-1;
-
diff --git a/site_perl/pkg_svc.pm b/site_perl/pkg_svc.pm
deleted file mode 100644 (file)
index 517125c..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-package FS::pkg_svc;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::Record qw(fields hfields qsearchs);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(hfields);
-
-=head1 NAME
-
-FS::pkg_svc - Object methods for pkg_svc records
-
-=head1 SYNOPSIS
-
-  use FS::pkg_svc;
-
-  $record = create FS::pkg_svc \%hash;
-  $record = create FS::pkg_svc { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::pkg_svc record links a billing item definition (see L<FS::part_pkg>) to
-a service definition (see L<FS::part_svc>).  FS::pkg_svc inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item pkgpart - Billing item definition (see L<FS::part_pkg>)
-
-=item svcpart - Service definition (see L<FS::part_svc>)
-
-=item quantity - Quantity of this service definition that this billing item
-definition includes
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Create a new record.  To add the record to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('pkg_svc')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('pkg_svc',$hashref);
-
-}
-
-=item insert
-
-Adds this record to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Deletes this record from the database.  If there is an error, returns the
-error, otherwise returns false.
-
-=cut
-
-sub delete {
-  my($self)=@_;
-
-  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a pkg_svc record!" unless $old->table eq "pkg_svc";
-  return "Can't change pkgpart!"
-    if $old->getfield('pkgpart') ne $new->getfield('pkgpart');
-  return "Can't change svcpart!"
-    if $old->getfield('svcpart') ne $new->getfield('svcpart');
-
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid record.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert and replace
-methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a pkg_svc record!" unless $self->table eq "pkg_svc";
-  my($recref) = $self->hashref;
-
-  my($error);
-  return $error if $error =
-    $self->ut_number('pkgpart')
-    || $self->ut_number('svcpart')
-    || $self->ut_number('quantity')
-  ;
-
-  return "Unknown pkgpart!"
-    unless qsearchs('part_pkg',{'pkgpart'=> $self->getfield('pkgpart')});
-
-  return "Unknown svcpart!"
-    unless qsearchs('part_svc',{'svcpart'=> $self->getfield('svcpart')});
-
-  ''; #no error
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::part_pkg>, L<FS::part_svc>, schema.html from the base
-documentation.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-1
-added hfields
-ivan@sisd.com 97-nov-13
-
-pod ivan@sisd.com 98-sep-22
-
-=cut
-
-1;
-
diff --git a/site_perl/svc_acct.pm b/site_perl/svc_acct.pm
deleted file mode 100644 (file)
index a43af6b..0000000
+++ /dev/null
@@ -1,557 +0,0 @@
-package FS::svc_acct;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK $nossh_hack $conf $dir_prefix @shells
-            $shellmachine @saltset @pw_set);
-use Exporter;
-use FS::Conf;
-use FS::Record qw(fields qsearchs);
-use FS::SSH qw(ssh);
-use FS::cust_svc;
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-$conf = new FS::Conf;
-$dir_prefix = $conf->config('home');
-@shells = $conf->config('shells');
-$shellmachine = $conf->config('shellmachine');
-
-@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
-@pw_set = ( 'a'..'z', 'A'..'Z', '0'..'9', '(', ')', '#', '!', '.', ',' );
-
-#not needed in 5.004 #srand($$|time);
-
-=head1 NAME
-
-FS::svc_acct - Object methods for svc_acct records
-
-=head1 SYNOPSIS
-
-  use FS::svc_acct;
-
-  $record = create FS::svc_acct \%hash;
-  $record = create FS::svc_acct { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-  $error = $record->suspend;
-
-  $error = $record->unsuspend;
-
-  $error = $record->cancel;
-
-=head1 DESCRIPTION
-
-An FS::svc_acct object represents an account.  FS::svc_acct inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item svcnum - primary key (assigned automatcially for new accounts)
-
-=item username
-
-=item _password - generated if blank
-
-=item popnum - Point of presence (see L<FS::svc_acct_pop>)
-
-=item uid
-
-=item gid
-
-=item finger - GECOS
-
-=item dir - set automatically if blank (and uid is not)
-
-=item shell
-
-=item quota - (unimplementd)
-
-=item slipip - IP address
-
-=item radius_I<Radius_Attribute> - I<Radius-Attribute>
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new account.  To add the account to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('svc_acct')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('svc_acct',$hashref);
-
-}
-
-=item insert
-
-Adds this account to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
-defined.  An FS::cust_svc record will be created and inserted.
-
-If the configuration value (see L<FS::Conf>) shellmachine exists, and the 
-username, uid, and dir fields are defined, the command
-
-  useradd -d $dir -m -s $shell -u $uid $username
-
-is executed on shellmachine via ssh.  This behaviour can be surpressed by
-setting $FS::svc_acct::nossh_hack true.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-  my($error);
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  $error=$self->check;
-  return $error if $error;
-
-  return "Username ". $self->username. " in use"
-    if qsearchs('svc_acct',{'username'=> $self->username } );
-
-  my($part_svc) = qsearchs('part_svc',{ 'svcpart' => $self->svcpart });
-  return "Unkonwn svcpart" unless $part_svc;
-  return "uid in use"
-    if $part_svc->svc_acct__uid_flag ne 'F'
-      && qsearchs('svc_acct',{'uid'=> $self->uid } )
-      && $self->username !~ /^(hyla)?fax$/
-    ;
-
-  my($svcnum)=$self->svcnum;
-  my($cust_svc);
-  unless ( $svcnum ) {
-    $cust_svc=create FS::cust_svc ( {
-      'svcnum'  => $svcnum,
-      'pkgnum'  => $self->pkgnum,
-      'svcpart' => $self->svcpart,
-    } );
-    my($error) = $cust_svc->insert;
-    return $error if $error;
-    $svcnum = $self->svcnum($cust_svc->svcnum);
-  }
-
-  $error = $self->add;
-  if ($error) {
-    #$cust_svc->del if $cust_svc;
-    $cust_svc->delete if $cust_svc;
-    return $error;
-  }
-
-  my($username,$uid,$dir,$shell) = (
-    $self->username,
-    $self->uid,
-    $self->dir,
-    $self->shell,
-  );
-  if ( $username 
-       && $uid
-       && $dir
-       && $shellmachine
-       && ! $nossh_hack ) {
-    #one way
-    ssh("root\@$shellmachine",
-        "useradd -d $dir -m -s $shell -u $uid $username"
-    );
-    #another way
-    #ssh("root\@$shellmachine","/bin/mkdir $dir; /bin/chmod 711 $dir; ".
-    #  "/bin/cp -p /etc/skel/.* $dir 2>/dev/null; ".
-    #  "/bin/cp -pR /etc/skel/Maildir $dir 2>/dev/null; ".
-    #  "/bin/chown -R $uid $dir") unless $nossh_hack;
-  }
-
-  ''; #no error
-}
-
-=item delete
-
-Deletes this account from the database.  If there is an error, returns the
-error, otherwise returns false.
-
-The corresponding FS::cust_svc record will be deleted as well.
-
-If the configuration value (see L<FS::Conf>) shellmachine exists, the command:
-
-  userdel $username
-
-is executed on shellmachine via ssh.  This behaviour can be surpressed by
-setting $FS::svc_acct::nossh_hack true.
-
-=cut
-
-sub delete {
-  my($self)=@_;
-  my($error);
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  my($svcnum)=$self->getfield('svcnum');
-
-  $error = $self->del;
-  return $error if $error;
-
-  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum});  
-  $error = $cust_svc->del;
-  return $error if $error;
-
-  my($username) = $self->getfield('username');
-  if ( $username && $shellmachine && ! $nossh_hack ) {
-    ssh("root\@$shellmachine","userdel $username");
-  }
-
-  '';
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-If the configuration value (see L<FS::Conf>) shellmachine exists, and the 
-dir field has changed, the command:
-
-  [ -d $old_dir ] && (
-    chmod u+t $old_dir;
-    umask 022;
-    mkdir $new_dir;
-    cd $old_dir;
-    find . -depth -print | cpio -pdm $new_dir;
-    chmod u-t $new_dir;
-    chown -R $uid.$gid $new_dir;
-    rm -rf $old_dir
-  )
-
-is executed on shellmachine via ssh.  This behaviour can be surpressed by
-setting $FS::svc_acct::nossh_hack true.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  my($error);
-
-  return "(Old) Not a svc_acct record!" unless $old->table eq "svc_acct";
-  return "Can't change svcnum!"
-    unless $old->getfield('svcnum') eq $new->getfield('svcnum');
-
-  return "Username in use"
-    if $old->getfield('username') ne $new->getfield('username') &&
-      qsearchs('svc_acct',{'username'=> $new->getfield('username') } );
-
-  return "Can't change uid!"
-    if $old->getfield('uid') ne $new->getfield('uid');
-
-  #change homdir when we change username
-  if ( $old->getfield('username') ne $new->getfield('username') ) {
-    $new->setfield('dir','');
-  }
-
-  $error=$new->check;
-  return $error if $error;
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  $error = $new->rep($old);
-  return $error if $error;
-
-  my($old_dir,$new_dir)=( $old->getfield('dir'),$new->getfield('dir') );
-  my($uid,$gid)=( $new->getfield('uid'), $new->getfield('gid') );
-  if ( $old_dir
-       && $new_dir
-       && $old_dir ne $new_dir
-       && ! $nossh_hack
-  ) {
-    ssh("root\@$shellmachine","[ -d $old_dir ] && ".
-                 "( chmod u+t $old_dir; ". #turn off qmail delivery
-                 "umask 022; mkdir $new_dir; cd $old_dir; ".
-                 "find . -depth -print | cpio -pdm $new_dir; ".
-                 "chmod u-t $new_dir; chown -R $uid.$gid $new_dir; ".
-                 "rm -rf $old_dir". 
-                 ")"
-    );
-  }
-
-  ''; #no error
-}
-
-=item suspend
-
-Suspends this account by prefixing *SUSPENDED* to the password.  If there is an
-error, returns the error, otherwise returns false.
-
-Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
-
-=cut
-
-sub suspend {
-  my($old) = @_;
-  my(%hash) = $old->hash;
-  unless ( $hash{_password} =~ /^\*SUSPENDED\* / ) {
-    $hash{_password} = '*SUSPENDED* '.$hash{_password};
-    my($new) = create FS::svc_acct ( \%hash );
-#    $new->replace($old);
-    $new->rep($old); #to avoid password checking :)
-  } else {
-    ''; #no error (already suspended)
-  }
-
-}
-
-=item unsuspend
-
-Unsuspends this account by removing *SUSPENDED* from the password.  If there is
-an error, returns the error, otherwise returns false.
-
-Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
-
-=cut
-
-sub unsuspend {
-  my($old) = @_;
-  my(%hash) = $old->hash;
-  if ( $hash{_password} =~ /^\*SUSPENDED\* (.*)$/ ) {
-    $hash{_password} = $1;
-    my($new) = create FS::svc_acct ( \%hash );
-#    $new->replace($old);
-    $new->rep($old); #to avoid password checking :)
-  } else {
-    ''; #no error (already unsuspended)
-  }
-}
-
-=item cancel
-
-Just returns false (no error) for now.
-
-Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
-
-=cut
-
-# Usage: $error = $record -> cancel;
-sub cancel {
-  ''; #stub (no error) - taken care of in delete
-}
-
-=item check
-
-Checks all fields to make sure this is a valid service.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert and replace
-methods.
-
-Sets any fixed values; see L<FS::part_svc>.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a svc_acct record!" unless $self->table eq "svc_acct";
-  my($recref) = $self->hashref;
-
-  $recref->{svcnum} =~ /^(\d*)$/ or return "Illegal svcnum";
-  $recref->{svcnum} = $1;
-
-  #get part_svc
-  my($svcpart);
-  my($svcnum)=$self->getfield('svcnum');
-  if ($svcnum) {
-    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum});
-    return "Unknown svcnum" unless $cust_svc; 
-    $svcpart=$cust_svc->svcpart;
-  } else {
-    $svcpart=$self->getfield('svcpart');
-  }
-  my($part_svc)=qsearchs('part_svc',{'svcpart'=>$svcpart});
-  return "Unkonwn svcpart" unless $part_svc;
-
-  #set fixed fields from part_svc
-  my($field);
-  foreach $field ( fields('svc_acct') ) {
-    if ( $part_svc->getfield('svc_acct__'. $field. '_flag') eq 'F' ) {
-      $self->setfield($field,$part_svc->getfield('svc_acct__'. $field) );
-    }
-  }
-
-  my($ulen)=$self->dbdef_table->column('username')->length;
-  $recref->{username} =~ /^([a-z0-9_\-]{2,$ulen})$/
-    or return "Illegal username";
-  $recref->{username} = $1;
-  $recref->{username} =~ /[a-z]/ or return "Illegal username";
-
-  $recref->{popnum} =~ /^(\d*)$/ or return "Illegal popnum";
-  $recref->{popnum} = $1;
-  return "Unkonwn popnum" unless
-    ! $recref->{popnum} ||
-    qsearchs('svc_acct_pop',{'popnum'=> $recref->{popnum} } );
-
-  unless ( $part_svc->getfield('svc_acct__uid_flag') eq 'F' ) {
-
-    $recref->{uid} =~ /^(\d*)$/ or return "Illegal uid";
-    $recref->{uid} = $1 eq '' ? $self->unique('uid') : $1;
-
-    $recref->{gid} =~ /^(\d*)$/ or return "Illegal gid";
-    $recref->{gid} = $1 eq '' ? $recref->{uid} : $1;
-    #not all systems use gid=uid
-    #you can set a fixed gid in part_svc
-
-    return "Only root can have uid 0"
-      if $recref->{uid} == 0 && $recref->{username} ne 'root';
-
-    my($error);
-    return $error if $error=$self->ut_textn('finger');
-
-    $recref->{dir} =~ /^([\/\w\-]*)$/
-      or return "Illegal directory";
-    $recref->{dir} = $1 || 
-      $dir_prefix . '/' . $recref->{username}
-      #$dir_prefix . '/' . substr($recref->{username},0,1). '/' . $recref->{username}
-    ;
-
-    unless ( $recref->{username} eq 'sync' ) {
-      my($shell);
-      if ( $shell = (grep $_ eq $recref->{shell}, @shells)[0] ) {
-        $recref->{shell} = $shell;
-      } else {
-        return "Illegal shell ". $self->shell;
-      }
-    } else {
-      $recref->{shell} = '/bin/sync';
-    }
-
-    $recref->{quota} =~ /^(\d*)$/ or return "Illegal quota (unimplemented)";
-    $recref->{quota} = $1;
-
-  } else {
-    $recref->{gid} ne '' ? 
-      return "Can't have gid without uid" : ( $recref->{gid}='' );
-    $recref->{finger} ne '' ? 
-      return "Can't have finger-name without uid" : ( $recref->{finger}='' );
-    $recref->{dir} ne '' ? 
-      return "Can't have directory without uid" : ( $recref->{dir}='' );
-    $recref->{shell} ne '' ? 
-      return "Can't have shell without uid" : ( $recref->{shell}='' );
-    $recref->{quota} ne '' ? 
-      return "Can't have quota without uid" : ( $recref->{quota}='' );
-  }
-
-  unless ( $part_svc->getfield('svc_acct__slipip_flag') eq 'F' ) {
-    unless ( $recref->{slipip} eq '0e0' ) {
-      $recref->{slipip} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/
-        or return "Illegal slipip". $self->slipip;
-      $recref->{slipip} = $1;
-    } else {
-      $recref->{slipip} = '0e0';
-    }
-
-  }
-
-  #arbitrary RADIUS stuff; allow ut_textn for now
-  foreach ( grep /^radius_/, fields('svc_acct') ) {
-    $self->ut_textn($_);
-  }
-
-  #generate a password if it is blank
-  $recref->{_password} = join('',map($pw_set[ int(rand $#pw_set) ], (0..7) ) )
-    unless ( $recref->{_password} );
-
-  #if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,16})$/ ) {
-  if ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([^\t\n]{4,8})$/ ) {
-    $recref->{_password} = $1.$3;
-    #uncomment this to encrypt password immediately upon entry, or run
-    #bin/crypt_pw in cron to give new users a window during which their
-    #password is available to techs, for faxing, etc.  (also be aware of 
-    #radius issues!)
-    #$recref->{password} = $1.
-    #  crypt($3,$saltset[int(rand(64))].$saltset[int(rand(64))]
-    #;
-  } elsif ( $recref->{_password} =~ /^((\*SUSPENDED\* )?)([\w\.\/]{13,24})$/ ) {
-    $recref->{_password} = $1.$3;
-  } elsif ( $recref->{_password} eq '*' ) {
-    $recref->{_password} = '*';
-  } else {
-    return "Illegal password";
-  }
-
-  ''; #no error
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-The remote commands should be configurable.
-
-The create method should set defaults from part_svc (like the check method
-sets fixed values).
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>,
-L<FS::SSH>, L<ssh>, L<FS::svc_acct_pop>, schema.html from the base
-documentation.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-16 - 21
-
-rewrite (among other things, now know about part_svc) ivan@sisd.com 98-mar-8
-
-Changed 'password' to '_password' because Pg6.3 reserves the password word
-       bmccane@maxbaud.net     98-apr-3
-
-username length and shell no longer hardcoded ivan@sisd.com 98-jun-28
-
-eww but needed: ignore uid duplicates for 'fax' and 'hylafax'
-ivan@sisd.com 98-jun-29
-
-$nossh_hack ivan@sisd.com 98-jul-13
-
-protections against UID/GID of 0 for incorrectly-setup RDBMSs (also
-in bin/svc_acct.export) ivan@sisd.com 98-jul-13
-
-arbitrary radius attributes ivan@sisd.com 98-aug-13
-
-/var/spool/freeside/conf/shellmachine ivan@sisd.com 98-aug-13
-
-pod and FS::conf ivan@sisd.com 98-sep-22
-
-=cut
-
-1;
-
diff --git a/site_perl/svc_acct_pop.pm b/site_perl/svc_acct_pop.pm
deleted file mode 100644 (file)
index a6f801f..0000000
+++ /dev/null
@@ -1,163 +0,0 @@
-package FS::svc_acct_pop;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::Record qw(fields qsearchs);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-=head1 NAME
-
-FS::svc_acct_pop - Object methods for svc_acct_pop records
-
-=head1 SYNOPSIS
-
-  use FS::svc_acct_pop;
-
-  $record = create FS::svc_acct_pop \%hash;
-  $record = create FS::svc_acct_pop { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::svc_acct object represents an point of presence.  FS::svc_acct_pop
-inherits from FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item popnum - primary key (assigned automatically for new accounts)
-
-=item city
-
-=item state
-
-=item ac - area code
-
-=item exch - exchange
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new point of presence (if only it were that easy!).  To add the 
-point of presence to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('svc_acct_pop')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('svc_acct_pop',$hashref);
-}
-
-=item insert
-
-Adds this point of presence to the databaes.  If there is an error, returns the
-error, otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Currently unimplemented.
-
-=cut
-
-sub delete {
-  my($self)=@_;
-  return "Can't (yet) delete POPs!";
-  #$self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not an svc_acct_pop record!"
-    unless $old->table eq "svc_acct_pop";
-  return "Can't change popnum!"
-    unless $old->getfield('popnum') eq $new->getfield('popnum');
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid point of presence.  If there is
-an error, returns the error, otherwise returns false.  Called by the insert
-and replace methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a svc_acct_pop record!" unless $self->table eq "svc_acct_pop";
-
-  my($error)=
-    $self->ut_numbern('popnum')
-      or $self->ut_text('city')
-      or $self->ut_text('state')
-      or $self->ut_number('ac')
-      or $self->ut_number('exch')
-  ;
-  return $error if $error;
-
-  '';
-
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-It should be renamed to part_pop.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<svc_acct>, schema.html from the base documentation.
-
-=head1 HISTORY
-
-Class dealing with pops 
-
-ivan@sisd.com 98-mar-8 
-
-pod ivan@sisd.com 98-sep-23
-
-=cut
-
-1;
-
diff --git a/site_perl/svc_acct_sm.pm b/site_perl/svc_acct_sm.pm
deleted file mode 100644 (file)
index c87ed2c..0000000
+++ /dev/null
@@ -1,350 +0,0 @@
-package FS::svc_acct_sm;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK $nossh_hack $conf $shellmachine @qmailmachines);
-use Exporter;
-use FS::Record qw(fields qsearch qsearchs);
-use FS::cust_svc;
-use FS::SSH qw(ssh);
-use FS::Conf;
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-$conf = new FS::Conf;
-
-$shellmachine = $conf->exists('qmailmachines')
-                ? $conf->config('shellmachine')
-                : '';
-
-=head1 NAME
-
-FS::svc_acct_sm - Object methods for svc_acct_sm records
-
-=head1 SYNOPSIS
-
-  use FS::svc_acct_sm;
-
-  $record = create FS::svc_acct_sm \%hash;
-  $record = create FS::svc_acct_sm { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-  $error = $record->suspend;
-
-  $error = $record->unsuspend;
-
-  $error = $record->cancel;
-
-=head1 DESCRIPTION
-
-An FS::svc_acct object represents a virtual mail alias.  FS::svc_acct inherits
-from FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item svcnum - primary key (assigned automatcially for new accounts)
-
-=item domsvc - svcnum of the virtual domain (see L<FS::svc_domain>)
-
-=item domuid - uid of the target account (see L<FS::svc_acct>)
-
-=item domuser - virtual username
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new virtual mail alias.  To add the virtual mail alias to the
-database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('svc_acct_sm')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('svc_acct_sm',$hashref);
-
-}
-
-=item insert
-
-Adds this virtual mail alias to the database.  If there is an error, returns
-the error, otherwise returns false.
-
-The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
-defined.  An FS::cust_svc record will be created and inserted.
-
-If the configuration values (see L<FS::Conf>) shellmachine and qmailmachines
-exist, and domuser is `*' (meaning a catch-all mailbox), the command:
-
-  [ -e $dir/.qmail-$qdomain-default ] || {
-    touch $dir/.qmail-$qdomain-default;
-    chown $uid:$gid $dir/.qmail-$qdomain-default;
-  }
-
-is executed on shellmachine via ssh (see L<dot-qmail/"EXTENSION ADDRESSES">).
-This behaviour can be surpressed by setting $FS::svc_acct_sm::nossh_hack true.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-  my($error);
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  $error=$self->check;
-  return $error if $error;
-
-  return "Domain username (domuser) in use for this domain (domsvc)"
-    if qsearchs('svc_acct_sm',{ 'domuser'=> $self->domuser,
-                                'domsvc' => $self->domsvc,
-                              } );
-
-  return "First domain username (domuser) for domain (domsvc) must be " .
-         qq='*' (catch-all)!=
-    if $self->domuser ne '*' &&
-       ! qsearch('svc_acct_sm',{ 'domsvc' => $self->domsvc } );
-
-  my($svcnum)=$self->getfield('svcnum');
-  my($cust_svc);
-  unless ( $svcnum ) {
-    $cust_svc=create FS::cust_svc ( {
-      'svcnum'  => $svcnum,
-      'pkgnum'  => $self->getfield('pkgnum'),
-      'svcpart' => $self->getfield('svcpart'),
-    } );
-    my($error) = $cust_svc->insert;
-    return $error if $error;
-    $svcnum = $self->setfield('svcnum',$cust_svc->getfield('svcnum'));
-  }
-
-  $error = $self->add;
-  if ($error) {
-    $cust_svc->del if $cust_svc;
-    return $error;
-  }
-
-  my $svc_domain = qsearchs('svc_domain',{'svcnum'=> $self->domsvc } );
-  my $svc_acct = qsearchs('svc_acct',{'uid'=> $self->domuid } );
-  my($uid,$gid,$dir,$domain)=(
-    $svc_acct->getfield('uid'),
-    $svc_acct->getfield('gid'),
-    $svc_acct->getfield('dir'),
-    $svc_domain->getfield('domain')
-  );
-  my($qdomain)=$domain;
-  $qdomain =~ s/\./:/g; #see manpage for 'dot-qmail': EXTENSION ADDRESSES
-  ssh("root\@$shellmachine","[ -e $dir/.qmail-$qdomain-default ] || { touch $dir/.qmail-$qdomain-default; chown $uid:$gid $dir/.qmail-$qdomain-default; }")  
-    if ( ! $nossh_hack && $shellmachine && $dir && $self->domuser eq '*' );
-
-  ''; #no error
-
-}
-
-=item delete
-
-Deletes this virtual mail alias from the database.  If there is an error,
-returns the error, otherwise returns false.
-
-The corresponding FS::cust_svc record will be deleted as well.
-
-=cut
-
-sub delete {
-  my($self)=@_;
-  my($error);
-
-  my($svcnum)=$self->getfield('svcnum');
-
-  $error = $self->del;
-  return $error if $error;
-
-  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum});
-  $error = $cust_svc->del;
-  return $error if $error;
-
-  '';
-  
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  my($error);
-
-  return "(Old) Not a svc_acct_sm record!" unless $old->table eq "svc_acct_sm";
-  return "Can't change svcnum!"
-    unless $old->getfield('svcnum') eq $new->getfield('svcnum');
-
-  return "Domain username (domuser) in use for this domain (domsvc)"
-    if ( $old->domuser ne $new->domuser
-         || $old->domsvc  ne $new->domsvc
-       )  && qsearchs('svc_acct_sm',{
-         'domuser'=> $new->domuser,
-         'domsvc' => $new->domsvc,
-       } )
-     ;
-
-  $error=$new->check;
-  return $error if $error;
-
-  $error = $new->rep($old);
-  return $error if $error;
-
-  ''; #no error
-}
-
-=item suspend
-
-Just returns false (no error) for now.
-
-Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
-
-=cut
-
-sub suspend {
-  ''; #no error (stub)
-}
-
-=item unsuspend
-
-Just returns false (no error) for now.
-
-Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
-
-=cut
-
-sub unsuspend {
-  ''; #no error (stub)
-}
-
-=item cancel
-
-Just returns false (no error) for now.
-
-Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
-
-=cut
-
-sub cancel {
-  ''; #no error (stub)
-}
-
-=item check
-
-Checks all fields to make sure this is a valid virtual mail alias.  If there is
-an error, returns the error, otherwise returns false.  Called by the insert and
-replace methods.
-
-Sets any fixed values; see L<FS::part_svc>.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a svc_acct_sm record!" unless $self->table eq "svc_acct_sm";
-  my($recref) = $self->hashref;
-
-  $recref->{svcnum} =~ /^(\d*)$/ or return "Illegal svcnum";
-  $recref->{svcnum} = $1;
-
-  #get part_svc
-  my($svcpart);
-  my($svcnum)=$self->getfield('svcnum');
-  if ($svcnum) {
-    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum});
-    return "Unknown svcnum" unless $cust_svc; 
-    $svcpart=$cust_svc->svcpart;
-  } else {
-    $svcpart=$self->getfield('svcpart');
-  }
-  my($part_svc)=qsearchs('part_svc',{'svcpart'=>$svcpart});
-  return "Unkonwn svcpart" unless $part_svc;
-
-  #set fixed fields from part_svc
-  my($field);
-  foreach $field ( fields('svc_acct_sm') ) {
-    if ( $part_svc->getfield('svc_acct_sm__'. $field. '_flag') eq 'F' ) {
-      $self->setfield($field,$part_svc->getfield('svc_acct_sm__'. $field) );
-    }
-  }
-
-  $recref->{domuser} =~ /^(\*|[a-z0-9_\-]{2,32})$/
-    or return "Illegal domain username (domuser)";
-  $recref->{domuser} = $1;
-
-  $recref->{domsvc} =~ /^(\d+)$/ or return "Illegal domsvc";
-  $recref->{domsvc} = $1;
-  my($svc_domain);
-  return "Unknown domsvc" unless
-    $svc_domain=qsearchs('svc_domain',{'svcnum'=> $recref->{domsvc} } );
-
-  $recref->{domuid} =~ /^(\d+)$/ or return "Illegal uid";
-  $recref->{domuid} = $1;
-  my($svc_acct);
-  return "Unknown uid" unless
-    $svc_acct=qsearchs('svc_acct',{'uid'=> $recref->{domuid} } );
-
-  ''; #no error
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-The remote commands should be configurable.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>,
-L<FS::svc_acct>, L<FS::svc_domain>, L<FS::SSH>, L<ssh>, L<dot-qmail>,
-schema.html from the base documentation.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-16 - 21
-
-rewrite ivan@sisd.com 98-mar-10
-
-s/qsearchs/qsearch/ to eliminate warning ivan@sisd.com 98-apr-19
-
-uses conf/shellmachine and has an nossh_hack ivan@sisd.com 98-jul-14
-
-s/\./:/g in .qmail-domain:com ivan@sisd.com 98-aug-13 
-
-pod, FS::Conf, moved .qmail file from check to insert 98-sep-23
-
-=cut
-
-1;
-
diff --git a/site_perl/svc_domain.pm b/site_perl/svc_domain.pm
deleted file mode 100644 (file)
index 1ddd5b2..0000000
+++ /dev/null
@@ -1,539 +0,0 @@
-package FS::svc_domain;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK $whois_hack $conf $mydomain $smtpmachine);
-use Exporter;
-use Carp;
-use Mail::Internet;
-use Mail::Header;
-use Date::Format;
-use FS::Record qw(fields qsearch qsearchs);
-use FS::cust_svc;
-use FS::Conf;
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-$conf = new FS::Conf;
-
-$mydomain = $conf->config('domain');
-$smtpmachine = $conf->config('smtpmachine');
-
-my($internic)="/var/spool/freeside/conf/registries/internic";
-my($conf_tech)="$internic/tech_contact";
-my($conf_from)="$internic/from";
-my($conf_to)="$internic/to";
-my($nameservers)="$internic/nameservers";
-my($template)="$internic/template";
-
-open(TECH_CONTACT,$conf_tech) or die "Can't open $conf_tech: $!";
-my($tech_contact)=map {
-  /^(.*)$/ or die "Illegal line in $conf_tech!"; #yes, we trust the file
-  $1;
-} grep $_ !~ /^(#|$)/, <TECH_CONTACT>;
-close TECH_CONTACT;
-
-open(FROM,$conf_from) or die "Can't open $conf_from: $!";
-my($from)=map {
-  /^(.*)$/ or die "Illegal line in $conf_from!"; #yes, we trust the file
-  $1;
-} grep $_ !~ /^(#|$)/, <FROM>;
-close FROM;
-
-open(TO,$conf_to) or die "Can't open $conf_to: $!";
-my($to)=map {
-  /^(.*)$/ or die "Illegal line in $conf_to!"; #yes, we trust the file
-  $1;
-} grep $_ !~ /^(#|$)/, <TO>;
-close TO;
-
-open(NAMESERVERS,$nameservers) or die "Can't open $nameservers: $!";
-my(@nameservers)=map {
-  /^\s*\d+\.\d+\.\d+\.\d+\s+([^\s]+)\s*$/
-    or die "Illegal line in $nameservers!"; #yes, we trust the file
-  $1;
-} grep $_ !~ /^(#|$)/, <NAMESERVERS>;
-close NAMESERVERS;
-open(NAMESERVERS,$nameservers) or die "Can't open $nameservers: $!";
-my(@nameserver_ips)=map {
-  /^\s*(\d+\.\d+\.\d+\.\d+)\s+([^\s]+)\s*$/
-    or die "Illegal line in $nameservers!"; #yes, we trust the file
-  $1;
-} grep $_ !~ /^(#|$)/, <NAMESERVERS>;
-close NAMESERVERS;
-
-open(TEMPLATE,$template) or die "Can't open $template: $!";
-my(@template)=map {
-  /^(.*)$/ or die "Illegal line in $to!"; #yes, we trust the file
-  $1. "\n";
-} <TEMPLATE>;
-close TEMPLATE;
-
-=head1 NAME
-
-FS::svc_domain - Object methods for svc_domain records
-
-=head1 SYNOPSIS
-
-  use FS::svc_domain;
-
-  $record = create FS::svc_domain \%hash;
-  $record = create FS::svc_domain { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-  $error = $record->suspend;
-
-  $error = $record->unsuspend;
-
-  $error = $record->cancel;
-
-=head1 DESCRIPTION
-
-An FS::svc_domain object represents a domain.  FS::svc_domain inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item svcnum - primary key (assigned automatically for new accounts)
-
-=item domain
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Creates a new domain.  To add the domain to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('svc_domain')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('svc_domain',$hashref);
-
-}
-
-=item insert
-
-Adds this domain to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-The additional fields I<pkgnum> and I<svcpart> (see L<FS::cust_svc>) should be 
-defined.  An FS::cust_svc record will be created and inserted.
-
-The additional field I<action> should be set to I<N> for new domains or I<M>
-for transfers.
-
-A registration or transfer email will be submitted unless
-$FS::svc_domain::whois_hack is true.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-  my($error);
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  $error=$self->check;
-  return $error if $error;
-
-  return "Domain in use (here)"
-    if qsearchs('svc_domain',{'domain'=> $self->domain } );
-
-  my($whois)=(($self->_whois)[0]);
-  return "Domain in use (see whois)"
-    if ( $self->action eq "N" && $whois !~ /^No match for/ );
-  return "Domain not found (see whois)"
-    if ( $self->action eq "M" && $whois =~ /^No match for/ );
-
-  my($svcnum)=$self->getfield('svcnum');
-  my($cust_svc);
-  unless ( $svcnum ) {
-    $cust_svc=create FS::cust_svc ( {
-      'svcnum'  => $svcnum,
-      'pkgnum'  => $self->getfield('pkgnum'),
-      'svcpart' => $self->getfield('svcpart'),
-    } );
-    my($error) = $cust_svc->insert;
-    return $error if $error;
-    $svcnum = $self->setfield('svcnum',$cust_svc->getfield('svcnum'));
-  }
-
-  $error = $self->add;
-  if ($error) {
-    $cust_svc->del if $cust_svc;
-    return $error;
-  }
-
-  $self->submit_internic unless $whois_hack;
-
-  ''; #no error
-}
-
-=item delete
-
-Deletes this domain from the database.  If there is an error, returns the
-error, otherwise returns false.
-
-The corresponding FS::cust_svc record will be deleted as well.
-
-=cut
-
-sub delete {
-  my($self)=@_;
-  my($error);
-
-  my($svcnum)=$self->getfield('svcnum');
-  
-  $error = $self->del;
-  return $error if $error;
-
-  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum});  
-  $error = $cust_svc->del;
-  return $error if $error;
-
-  '';
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  my($error);
-
-  return "(Old) Not a svc_domain record!" unless $old->table eq "svc_domain";
-  return "Can't change svcnum!"
-    unless $old->getfield('svcnum') eq $new->getfield('svcnum');
-
-  return "Can't change domain - reorder."
-    if $old->getfield('domain') ne $new->getfield('domain'); 
-
-  $error=$new->check;
-  return $error if $error;
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  $error = $new->rep($old);
-  return $error if $error;
-
-  '';
-
-}
-
-=item suspend
-
-Just returns false (no error) for now.
-
-Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
-
-=cut
-
-sub suspend {
-  ''; #no error (stub)
-}
-
-=item unsuspend
-
-Just returns false (no error) for now.
-
-Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
-
-=cut
-
-sub unsuspend {
-  ''; #no error (stub)
-}
-
-=item cancel
-
-Just returns false (no error) for now.
-
-Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
-
-=cut
-
-sub cancel {
-  ''; #no error (stub)
-}
-
-=item check
-
-Checks all fields to make sure this is a valid domain.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert and replace
-methods.
-
-Sets any fixed values; see L<FS::part_svc>.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a svc_domain record!" unless $self->table eq "svc_domain";
-  my($recref) = $self->hashref;
-
-  $recref->{svcnum} =~ /^(\d*)$/ or return "Illegal svcnum";
-  $recref->{svcnum} = $1;
-
-  #get part_svc (and pkgnum)
-  my($svcpart,$pkgnum);
-  my($svcnum)=$self->getfield('svcnum');
-  if ($svcnum) {
-    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum});
-    return "Unknown svcnum" unless $cust_svc; 
-    $svcpart=$cust_svc->svcpart;
-    $pkgnum=$cust_svc->pkgnum;
-  } else {
-    $svcpart=$self->svcpart;
-    $pkgnum=$self->pkgnum;
-  }
-  my($part_svc)=qsearchs('part_svc',{'svcpart'=>$svcpart});
-  return "Unkonwn svcpart" unless $part_svc;
-
-  #set fixed fields from part_svc
-  my($field);
-  foreach $field ( fields('svc_acct') ) {
-    if ( $part_svc->getfield('svc_domain__'. $field. '_flag') eq 'F' ) {
-      $self->setfield($field,$part_svc->getfield('svc_domain__'. $field) );
-    }
-  }
-
-  unless ( $whois_hack ) {
-    unless ( $self->email ) { #find out an email address
-      my(@svc_acct);
-      foreach ( qsearch('cust_svc',{'pkgnum'=>$pkgnum}) ) {
-        my($svc_acct)=qsearchs('svc_acct',{'svcnum'=>$_->svcnum});
-        push @svc_acct, $svc_acct if $svc_acct;
-      }
-
-      if ( scalar(@svc_acct) == 0 ) {
-        return "Must order an account first";
-      } elsif ( scalar(@svc_acct) > 1 ) {
-        return "More than one account in package ". $pkgnum. ": specify admin contact email";
-      } else {
-        $self->email($svc_acct[0]->username. '@'. $mydomain);
-      }
-    }
-  }
-
-  #if ( $recref->{domain} =~ /^([\w\-\.]{1,22})\.(com|net|org|edu)$/ ) {
-  if ( $recref->{domain} =~ /^([\w\-]{1,22})\.(com|net|org|edu)$/ ) {
-    $recref->{domain} = "$1.$2";
-  # hmmmmmmmm.
-  } elsif ( $whois_hack && $recref->{domain} =~ /^([\w\-\.]+)$/ ) {
-    $recref->{domain} = $1;
-  } else {
-    return "Illegal domain ". $recref->{domain}.
-           " (or unknown registry - try \$whois_hack)";
-  }
-
-  $recref->{action} =~ /^(M|N)$/ or return "Illegal action";
-  $recref->{action} = $1;
-
-  $self->ut_textn('purpose');
-
-}
-
-=item _whois
-
-Executes the command:
-
-  whois do $domain
-
-and returns the output.
-
-(Always returns I<No match for domian "$domain".> if
-$FS::svc_domain::whois_hack is set true.)
-
-=cut
-
-sub _whois {
-  my($self)=@_;
-  my($domain)=$self->domain;
-  return ( "No match for domain \"$domain\"." ) if $whois_hack;
-  open(WHOIS,"whois do $domain |");
-  return <WHOIS>;
-}
-
-=item submit_internic
-
-Submits a registration email for this domain.
-
-=cut
-
-sub submit_internic {
-  my($self)=@_;
-
-  my($cust_pkg)=qsearchs('cust_pkg',{'pkgnum'=>$self->pkgnum});
-  return unless $cust_pkg;
-  my($cust_main)=qsearchs('cust_main',{'custnum'=>$cust_pkg->custnum});
-  return unless $cust_main;
-
-  my(%subs)=(
-    'action'       => $self->action,
-    'purpose'      => $self->purpose,
-    'domain'       => $self->domain,
-    'company'      => $cust_main->company 
-                        || $cust_main->getfield('first'). ' '.
-                           $cust_main->getfield('last')
-                      ,
-    'city'         => $cust_main->city,
-    'state'        => $cust_main->state,
-    'zip'          => $cust_main->zip,
-    'country'      => $cust_main->country,
-    'last'         => $cust_main->getfield('last'),
-    'first'        => $cust_main->getfield('first'),
-    'daytime'      => $cust_main->daytime,
-    'fax'          => $cust_main->fax,
-    'email'        => $self->email,
-    'tech_contact' => $tech_contact,
-    'primary'      => shift @nameservers,
-    'primary_ip'   => shift @nameserver_ips,
-  );
-
-  #yuck
-  my(@xtemplate)=@template;
-  my(@body);
-  my($line);
-  OLOOP: while ( defined($line = shift @xtemplate) ) {
-
-    if ( $line =~ /^###LOOP###$/ ) {
-      my(@buffer);
-      LOADBUF: while ( defined($line = shift @xtemplate) ) {
-        last LOADBUF if ( $line =~ /^###ENDLOOP###$/ );
-        push @buffer, $line;
-      }
-      my(%lubs)=(
-        'address'      => $cust_main->address2 
-                            ? [ $cust_main->address1, $cust_main->address2 ]
-                            : [ $cust_main->address1 ]
-                          ,
-        'secondary'    => [ @nameservers ],
-        'secondary_ip' => [ @nameserver_ips ],
-      );
-      LOOP: while (1) {
-        my(@xbuffer)=@buffer;
-        SUBLOOP: while ( defined($line = shift @xbuffer) ) {
-          if ( $line =~ /###(\w+)###/ ) {
-            #last LOOP unless my($lub)=shift@{$lubs{$1}};
-            next OLOOP unless my $lub = shift @{$lubs{$1}};
-            $line =~ s/###(\w+)###/$lub/e;
-            redo SUBLOOP;
-          } else {
-            push @body, $line;
-          }
-        } #SUBLOOP
-      } #LOOP
-
-    }
-
-    if ( $line =~ /###(\w+)###/ ) {
-      #$line =~ s/###(\w+)###/$subs{$1}/eg;
-      $line =~ s/###(\w+)###/$subs{$1}/e;
-      redo OLOOP;
-    } else {
-      push @body, $line;
-    }
-
-  } #OLOOP
-
-  my($subject);
-  if ( $self->action eq "M" ) {
-    $subject = "MODIFY DOMAIN ". $self->domain;
-  } elsif ($self->action eq "N" ) { 
-    $subject = "NEW DOMAIN ". $self->domain;
-  } else {
-    croak "submit_internic called with action ". $self->action;
-  }
-
-  $ENV{SMTPHOSTS}=$smtpmachine;
-  $ENV{MAILADDRESS}=$from;
-  my($header)=Mail::Header->new( [
-    "From: $from",
-    "To: $to",
-    "Sender: $from",
-    "Reply-To: $from",
-    "Date: ". time2str("%a, %d %b %Y %X %z",time),
-    "Subject: $subject",
-  ] );
-
-  my($msg)=Mail::Internet->new(
-    'Header' => $header,
-    'Body' => \@body,
-  );
-
-  $msg->smtpsend or die "Can't send registration email"; #die? warn?
-
-}
-
-=back
-
-=head1 BUGS
-
-It doesn't properly override FS::Record yet.
-
-All BIND/DNS fields should be included (and exported).
-
-All registries should be supported.
-
-Not all configuration access is through FS::Conf!
-
-Should change action to a real field.
-
-=head1 SEE ALSO
-
-L<FS::Record>, L<FS::Conf>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>,
-L<FS::SSH>, L<ssh>, L<dot-qmail>, schema.html from the base documentation,
-config.html from the base documentation.
-
-=head1 HISTORY
-
-ivan@voicenet.com 97-jul-21
-
-rewrite ivan@sisd.com 98-mar-10
-
-add internic bits ivan@sisd.com 98-mar-14
-
-Changed 'day' to 'daytime' because Pg6.3 reserves the day word
-       bmccane@maxbaud.net     98-apr-3
-
-/var/spool/freeside/conf/registries/internic/, Mail::Internet, etc.
-ivan@sisd.com 98-jul-17-19
-
-pod, some FS::Conf (not complete) ivan@sisd.com 98-sep-23
-
-=cut
-
-1;
-
-
diff --git a/site_perl/table_template-svc.pm b/site_perl/table_template-svc.pm
deleted file mode 100644 (file)
index a8cbaed..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/usr/local/bin/perl -Tw
-#
-# ivan@voicenet.com 97-jul-21
-
-package FS::svc_table;
-
-use strict;
-use Exporter;
-use FS::Record qw(fields qsearchs);
-
-@FS::svc_table::ISA = qw(FS::Record Exporter);
-
-# Usage: $record = create FS::svc_table ( \%hash );
-#        $record = create FS::svc_table ( { field=>value, ... } );
-sub create {
-  my($proto,$hashref)=@_;
-
-  my($field);
-  foreach $field (fields('svc_table')) {
-    $hashref->{$field}='' unless defined $hashref->{$field};
-  }
-
-  $proto->new('svc_table',$hashref);
-
-}
-
-# Usage: $error = $record -> insert;
-sub insert {
-  my($self)=@_;
-  my($error);
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-
-  $error=$self->check;
-  return $error if $error;
-
-  $error = $self->add;
-  return $error if $error;
-
-  ''; #no error
-}
-
-# Usage: $error = $record -> delete;
-sub delete {
-  my($self)=@_;
-  my($error);
-
-  $error = $self->del;
-  return $error if $error;
-
-}
-
-# Usage: $error = $newrecord -> replace($oldrecord)
-sub replace {
-  my($new,$old)=@_;
-  my($error);
-
-  return "(Old) Not a svc_table record!" unless $old->table eq "svc_table";
-  return "Can't change svcnum!"
-    unless $old->getfield('svcnum') eq $new->getfield('svcnum');
-
-  $error=$new->check;
-  return $error if $error;
-
-  $error = $new->rep($old);
-  return $error if $error;
-
-  ''; #no error
-}
-
-# Usage: $error = $record -> suspend;
-sub suspend {
-  ''; #no error (stub)
-}
-
-# Usage: $error = $record -> unsuspend;
-sub unsuspend {
-  ''; #no error (stub)
-}
-
-# Usage: $error = $record -> cancel;
-sub cancel {
-  ''; #no error (stub)
-}
-
-# Usage: $error = $record -> check;
-sub check {
-  my($self)=@_;
-  return "Not a svc_table record!" unless $self->table eq "svc_table";
-  my($recref) = $self->hashref;
-
-  $recref->{svcnum} =~ /^(\d+)$/ or return "Illegal svcnum";
-  $recref->{svcnum} = $1;
-  return "Unknown svcnum" unless
-    qsearchs('cust_svc',{'svcnum'=> $recref->{svcnum} } );
-
-  #DATA CHECKS GO HERE!
-
-  ''; #no error
-}
-
-1;
-
diff --git a/site_perl/table_template-unique.pm b/site_perl/table_template-unique.pm
deleted file mode 100644 (file)
index 32b7e69..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/local/bin/perl -Tw
-#
-# ivan@voicenet.com 97-jul-1
-# 
-# added hfields
-# ivan@sisd.com 97-nov-13
-
-package FS::table_name;
-
-use strict;
-use Exporter;
-#use FS::UID qw(getotaker);
-use FS::Record qw(fields hfields qsearch qsearchs);
-
-@FS::table_name::ISA = qw(FS::Record Exporter);
-@FS::table_name::EXPORT_OK = qw(hfields);
-
-# Usage: $record = create FS::table_name ( \%hash );
-#        $record = create FS::table_name ( { field=>value, ... } );
-sub create {
-  my($proto,$hashref)=@_;
-
-  my($field);
-  foreach $field (fields('table_name')) {
-    $hashref->{$field}='' unless defined $hashref->{$field};
-  }
-
-  $proto->new('table_name',$hashref);
-}
-
-# Usage: $error = $record -> insert;
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-# Usage: $error = $record -> delete;
-sub delete {
-  my($self)=@_;
-
-  $self->del;
-}
-
-# Usage: $error = $newrecord -> replace($oldrecord)
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a table_name record!" unless $old->table eq "table_name";
-  return "Can't change keyfield!"
-    unless $old->getfield('keyfield') eq $new->getfield('keyfield');
-  $new->check or
-  $new->rep($old);
-}
-
-# Usage: $error = $record -> check;
-sub check {
-  my($self)=@_;
-  return "Not a table_name record!" unless $self->table eq "table_name";
-  my($recref) = $self->hashref;
-
-  ''; #no error
-}
-
-1;
-
diff --git a/site_perl/table_template.pm b/site_perl/table_template.pm
deleted file mode 100644 (file)
index cef2d92..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-#!/usr/local/bin/perl -Tw
-#
-# ivan@voicenet.com 97-jul-1
-# 
-# added hfields
-# ivan@sisd.com 97-nov-13
-
-package FS::table_name;
-
-use strict;
-use Exporter;
-#use FS::UID qw(getotaker);
-use FS::Record qw(hfields qsearch qsearchs);
-
-@FS::table_name::ISA = qw(FS::Record Exporter);
-@FS::table_name::EXPORT_OK = qw(hfields);
-
-# Usage: $record = create FS::table_name ( \%hash );
-#        $record = create FS::table_name ( { field=>value, ... } );
-sub create {
-  my($proto,$hashref)=@_;
-
-  my($field);
-  foreach $field (fields('table_name')) {
-    $hashref->{$field}='' unless defined $hashref->{$field};
-  }
-
-  $proto->new('table_name',$hashref);
-
-}
-
-# Usage: $error = $record -> insert;
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-# Usage: $error = $record -> delete;
-sub delete {
-  my($self)=@_;
-
-  $self->del;
-}
-
-# Usage: $error = $newrecord -> replace($oldrecord)
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a table_name record!" unless $old->table eq "table_name";
-
-  $new->check or
-  $new->rep($old);
-}
-
-# Usage: $error = $record -> check;
-sub check {
-  my($self)=@_;
-  return "Not a table_name record!" unless $self->table eq "table_name";
-  my($recref) = $self->hashref;
-
-  ''; #no error
-}
-
-1;
-
diff --git a/site_perl/type_pkgs.pm b/site_perl/type_pkgs.pm
deleted file mode 100644 (file)
index a715796..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-package FS::type_pkgs;
-
-use strict;
-use vars qw(@ISA @EXPORT_OK);
-use Exporter;
-use FS::Record qw(fields qsearchs);
-
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(fields);
-
-=head1 NAME
-
-FS::type_pkgs - Object methods for type_pkgs records
-
-=head1 SYNOPSIS
-
-  use FS::type_pkgs;
-
-  $record = create FS::type_pkgs \%hash;
-  $record = create FS::type_pkgs { 'column' => 'value' };
-
-  $error = $record->insert;
-
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
-  $error = $record->check;
-
-=head1 DESCRIPTION
-
-An FS::type_pkgs record links an agent type (see L<FS::agent_type>) to a
-billing item definition (see L<FS::part_pkg>).  FS::type_pkgs inherits from
-FS::Record.  The following fields are currently supported:
-
-=over 4
-
-=item typenum - Agent type, see L<FS::agent_type>
-
-=item pkgpart - Billing item definition, see L<FS::part_pkg>
-
-=back
-
-=head1 METHODS
-
-=over 4
-
-=item create HASHREF
-
-Create a new record.  To add the record to the database, see L<"insert">.
-
-=cut
-
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my($field);
-  #foreach $field (fields('type_pkgs')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('type_pkgs',$hashref);
-
-}
-
-=item insert
-
-Adds this record to the database.  If there is an error, returns the error,
-otherwise returns false.
-
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  $self->check or
-  $self->add;
-}
-
-=item delete
-
-Deletes this record from the database.  If there is an error, returns the
-error, otherwise returns false.
-
-=cut
-
-sub delete {
-  my($self)=@_;
-
-  $self->del;
-}
-
-=item replace OLD_RECORD
-
-Replaces OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
-sub replace {
-  my($new,$old)=@_;
-  return "(Old) Not a type_pkgs record!" unless $old->table eq "type_pkgs";
-
-  $new->check or
-  $new->rep($old);
-}
-
-=item check
-
-Checks all fields to make sure this is a valid record.  If there is an error,
-returns the error, otherwise returns false.  Called by the insert and replace
-methods.
-
-=cut
-
-sub check {
-  my($self)=@_;
-  return "Not a type_pkgs record!" unless $self->table eq "type_pkgs";
-  my($recref) = $self->hashref;
-
-  $recref->{typenum} =~ /^(\d+)$/ or return "Illegal typenum";
-  $recref->{typenum} = $1;
-  return "Unknown typenum"
-    unless qsearchs('agent_type',{'typenum'=>$recref->{typenum}});
-
-  $recref->{pkgpart} =~ /^(\d+)$/ or return "Illegal pkgpart";
-  $recref->{pkgpart} = $1;
-  return "Unknown pkgpart"
-    unless qsearchs('part_pkg',{'pkgpart'=>$recref->{pkgpart}});
-
-  ''; #no error
-}
-
-=back
-
-=head1 HISTORY
-
-Defines the relation between agent types and pkgparts
-(Which pkgparts can the different [types of] agents sell?)
-
-ivan@sisd.com 97-nov-13
-
-change to ut_ FS::Record, fixed bugs
-ivan@sisd.com 97-dec-10
-
-=cut
-
-1;
-
diff --git a/test/cgi-test b/test/cgi-test
new file mode 100755 (executable)
index 0000000..2dda484
--- /dev/null
@@ -0,0 +1,560 @@
+#!/usr/bin/perl -Tw
+#
+# $Id: cgi-test,v 1.3 2001-08-21 02:32:54 ivan Exp $
+#
+# This is the beginning of a test suite for the web interface.
+# It's also excellent for populating your database with some meaningful test
+# data.  (a derivative is used by the web demo)
+# It only works on an empty database (probably need empty counters too, and
+# no arbirary RADIUS attributes).
+# Usage: cgi-test http://base.freeside.url/with/path/ username password
+# (Yes, if you were properly paranoid and are using SSL, you'll need to get
+#  libwww-perl working with SSL to use this.)
+
+use strict;
+#use diagnostics;
+use subs qw( big_ugly_data_structure );
+use CGI;
+use LWP::UserAgent;
+
+my ( $base_url, $username, $password ) = ( shift, shift, shift );
+#trust 'em
+$base_url =~ /^(.*)$/; $base_url = $1;
+$username =~ /^(.*)$/; $username = $1;
+$password =~ /^(.*)$/; $password = $1;
+
+my @data = &big_ugly_data_structure;
+
+my $ua = new LWP::UserAgent;
+{
+  local $^W = 0;
+  eval '
+    sub LWP::UserAgent::get_basic_credentials {
+      #my $self = shift;
+      ( $username, $password );
+    }
+  ';
+}
+
+my $data;
+while ( $data = shift @data ) {
+  my $cgi = new CGI ( $data->{'params'} );
+  my $full_url = $base_url. $data->{'url'}. '?'. $cgi->query_string;
+  #my $request = new HTTP::Request( 'POST', $full_url );
+  my $request = new HTTP::Request( 'GET', $full_url );
+  my $response = $ua->request( $request );
+  if ( $response->is_redirect ) {
+    die "Unexpected redirect!\n".
+           "URL: $full_url\n".
+           "To: ". $response->base. "\n"
+    ;
+  } elsif ( $response->is_success ) {
+    my $location = $response->base;
+    my $expected_location = $data->{'location'};
+    #if ( $location =~ /^$base_url$expected_location$/ ) {
+    if ( $location eq $base_url. $expected_location ) {
+      #warn "cool, got expected response $location from $full_url\n";
+    } else {
+      die "Strange, regular response, but unexpected base!\n".
+        "URL: $full_url\n".
+        "Base    : ". $response->base. "\n".
+        "Expected: $base_url$expected_location\n".
+        "Output: ". $response->content. "\n"
+      ;
+    }
+  } elsif ( $response->is_error ) {
+    die "Strange, I got an error\n".
+        "URL: $full_url\n".
+        "Error: ". $response->error_as_HTML. "\n".
+        "Output: ". $response->content. "\n"
+    ;
+  } elsif ( $response->is_info ) {
+    die "Strange, I got an info reponse\n".
+        "URL: $full_url\n".
+        "Output: ". $response->content. "\n"
+    ;
+  } else {
+    die "Really strange, got an unrecognized response from LWP::UserAgent!\n";
+  }
+}
+
+#---
+
+sub big_ugly_data_structure {
+
+  (
+    { 'url'      => 'edit/process/part_svc.cgi',
+      'params'   => {
+                      'svcpart' => '',
+                      'svc'     => 'Shell',
+                      'svcdb'   => 'svc_acct',
+                      'svc_acct__popnum_flag' => '',
+                      'svc_acct__popnum' => '',
+                      'svc_acct__dir_flag' => '',
+                      'svc_acct__dir' => '',
+                      'svc_acct__username_flag' => '',
+                      'svc_acct__username' => '',
+                      'svc_acct__uid_flag' => '',
+                      'svc_acct__uid' => '',
+                      'svc_acct__quota_flag' => 'F',
+                      'svc_acct__quota' => '10',
+                      'svc_acct__slipip_flag' => 'F',
+                      'svc_acct__slipip' => '',
+                      'svc_acct___password_flag' => '',
+                      'svc_acct___password' => '',
+                      'svc_acct__gid_flag' => '',
+                      'svc_acct__gid' => '',
+                      'svc_acct__shell_flag' => 'D',
+                      'svc_acct__shell' => '/bin/sh',
+                      'svc_acct__finger_flag' => '',
+                      'svc_acct__finger' => '',
+                      'svc_domain__domain_flag' => '',
+                      'svc_domain__domain' => '',
+                      'svc_acct_sm__domuser_flag' => '',
+                      'svc_acct_sm__domuser' => '',
+                      'svc_acct_sm__domuid_flag' => '',
+                      'svc_acct_sm__domuid' => '',
+                      'svc_acct_sm__domsvc_flag' => '',
+                      'svc_acct_sm__domsvc' => '',
+                    },
+      'location' => 'browse/part_svc.cgi',
+    },
+    { 'url'      => 'edit/process/part_svc.cgi',
+      'params'   => {
+                      'svcpart' => '',
+                      'svc'     => 'SLIP/PPP',
+                      'svcdb'   => 'svc_acct',
+                      'svc_acct__popnum_flag' => '',
+                      'svc_acct__popnum' => '',
+                      'svc_acct__dir_flag' => '',
+                      'svc_acct__dir' => '',
+                      'svc_acct__username_flag' => '',
+                      'svc_acct__username' => '',
+                      'svc_acct__uid_flag' => '',
+                      'svc_acct__uid' => '',
+                      'svc_acct__quota_flag' => 'F',
+                      'svc_acct__quota' => '10',
+                      'svc_acct__slipip_flag' => 'D',
+                      'svc_acct__slipip' => '0.0.0.0',
+                      'svc_acct___password_flag' => '',
+                      'svc_acct___password' => '',
+                      'svc_acct__gid_flag' => '',
+                      'svc_acct__gid' => '',
+                      'svc_acct__shell_flag' => 'D',
+                      'svc_acct__shell' => '/bin/sh',
+                      'svc_acct__finger_flag' => '',
+                      'svc_acct__finger' => '',
+                      'svc_domain__domain_flag' => '',
+                      'svc_domain__domain' => '',
+                      'svc_acct_sm__domuser_flag' => '',
+                      'svc_acct_sm__domuser' => '',
+                      'svc_acct_sm__domuid_flag' => '',
+                      'svc_acct_sm__domuid' => '',
+                      'svc_acct_sm__domsvc_flag' => '',
+                      'svc_acct_sm__domsvc' => '',
+                    },
+      'location' => 'browse/part_svc.cgi',
+    },
+    { 'url'      => 'edit/process/part_svc.cgi',
+      'params'   => {
+                      'svcpart' => '',
+                      'svc'     => 'POP Mailbox',
+                      'svcdb'   => 'svc_acct',,
+                      'svc_acct__popnum_flag' => 'F',
+                      'svc_acct__popnum' => '',
+                      'svc_acct__dir_flag' => '',
+                      'svc_acct__dir' => '',
+                      'svc_acct__username_flag' => '',
+                      'svc_acct__username' => '',
+                      'svc_acct__uid_flag' => '',
+                      'svc_acct__uid' => '',
+                      'svc_acct__quota_flag' => 'F',
+                      'svc_acct__quota' => '10',
+                      'svc_acct__slipip_flag' => 'F',
+                      'svc_acct__slipip' => '',
+                      'svc_acct___password_flag' => '',
+                      'svc_acct___password' => '',
+                      'svc_acct__gid_flag' => '',
+                      'svc_acct__gid' => '',
+                      'svc_acct__shell_flag' => 'F',
+                      'svc_acct__shell' => '/bin/passwd',
+                      'svc_acct__finger_flag' => '',
+                      'svc_acct__finger' => '',
+                      'svc_domain__domain_flag' => '',
+                      'svc_domain__domain' => '',
+                      'svc_acct_sm__domuser_flag' => '',
+                      'svc_acct_sm__domuser' => '',
+                      'svc_acct_sm__domuid_flag' => '',
+                      'svc_acct_sm__domuid' => '',
+                      'svc_acct_sm__domsvc_flag' => '',
+                      'svc_acct_sm__domsvc' => '',
+                    },
+      'location' => 'browse/part_svc.cgi',
+    },
+    { 'url'      => 'edit/process/part_svc.cgi',
+      'params'   => {
+                      'svcpart' => '',
+                      'svc'     => 'Domain',
+                      'svcdb'   => 'svc_domain',,
+                      'svc_acct__popnum_flag' => '',
+                      'svc_acct__popnum' => '',
+                      'svc_acct__dir_flag' => '',
+                      'svc_acct__dir' => '',
+                      'svc_acct__username_flag' => '',
+                      'svc_acct__username' => '',
+                      'svc_acct__uid_flag' => '',
+                      'svc_acct__uid' => '',
+                      'svc_acct__quota_flag' => '',
+                      'svc_acct__quota' => '',
+                      'svc_acct__slipip_flag' => '',
+                      'svc_acct__slipip' => '',
+                      'svc_acct___password_flag' => '',
+                      'svc_acct___password' => '',
+                      'svc_acct__gid_flag' => '',
+                      'svc_acct__gid' => '',
+                      'svc_acct__shell_flag' => '',
+                      'svc_acct__shell' => '',
+                      'svc_acct__finger_flag' => '',
+                      'svc_acct__finger' => '',
+                      'svc_domain__domain_flag' => '',
+                      'svc_domain__domain' => '',
+                      'svc_acct_sm__domuser_flag' => '',
+                      'svc_acct_sm__domuser' => '',
+                      'svc_acct_sm__domuid_flag' => '',
+                      'svc_acct_sm__domuid' => '',
+                      'svc_acct_sm__domsvc_flag' => '',
+                      'svc_acct_sm__domsvc' => '',
+                    },
+      'location' => 'browse/part_svc.cgi',
+    },
+    { 'url'      => 'edit/process/part_svc.cgi',
+      'params'   => {
+                      'svcpart' => '',
+                      'svc'     => 'Domain email alias',
+                      'svcdb'   => 'svc_acct_sm',,
+                      'svc_acct__popnum_flag' => '',
+                      'svc_acct__popnum' => '',
+                      'svc_acct__dir_flag' => '',
+                      'svc_acct__dir' => '',
+                      'svc_acct__username_flag' => '',
+                      'svc_acct__username' => '',
+                      'svc_acct__uid_flag' => '',
+                      'svc_acct__uid' => '',
+                      'svc_acct__quota_flag' => '',
+                      'svc_acct__quota' => '',
+                      'svc_acct__slipip_flag' => '',
+                      'svc_acct__slipip' => '',
+                      'svc_acct___password_flag' => '',
+                      'svc_acct___password' => '',
+                      'svc_acct__gid_flag' => '',
+                      'svc_acct__gid' => '',
+                      'svc_acct__shell_flag' => '',
+                      'svc_acct__shell' => '',
+                      'svc_acct__finger_flag' => '',
+                      'svc_acct__finger' => '',
+                      'svc_domain__domain_flag' => '',
+                      'svc_domain__domain' => '',
+                      'svc_acct_sm__domuser_flag' => '',
+                      'svc_acct_sm__domuser' => '',
+                      'svc_acct_sm__domuid_flag' => '',
+                      'svc_acct_sm__domuid' => '',
+                      'svc_acct_sm__domsvc_flag' => '',
+                      'svc_acct_sm__domsvc' => '',
+                    },
+      'location' => 'browse/part_svc.cgi',
+    },
+
+    { 'url'      => 'edit/process/part_pkg.cgi',
+      'params'   => {
+                      'pkgpart' => '',
+                      'pkg' => 'Personal SLIP/PPP',
+                      'comment' => '$30/setup, $19.99/month',
+                      'setup' => '30',
+                      'recur' => '19.99',
+                      'freq' => '1',
+                      'pkg_svc1' => '0',
+                      'pkg_svc2' => '1',
+                      'pkg_svc3' => '0',
+                      'pkg_svc4' => '0',
+                      'pkg_svc5' => '0',
+                    },
+      'location' => 'browse/part_pkg.cgi',
+    },
+    { 'url'      => 'edit/process/part_pkg.cgi',
+      'params'   => {
+                      'pkgpart' => '',
+                      'pkg' => 'Personal SLIP/PPP',
+                      'comment' => '$0/setup, $179.88/year',
+                      'setup' => '0',
+                      'recur' => '179.88',
+                      'freq' => '12',
+                      'pkg_svc1' => '0',
+                      'pkg_svc2' => '1',
+                      'pkg_svc3' => '0',
+                      'pkg_svc4' => '0',
+                      'pkg_svc5' => '0',
+                    },
+      'location' => 'browse/part_pkg.cgi',
+    },
+    { 'url'      => 'edit/process/part_pkg.cgi',
+      'params'   => {
+                      'pkgpart' => '',
+                      'pkg' => 'Personal POP mailbox',
+                      'comment' => '$10/setup, $5/month',
+                      'setup' => '10',
+                      'recur' => '5',
+                      'freq' => '1',
+                      'pkg_svc1' => '0',
+                      'pkg_svc2' => '0',
+                      'pkg_svc3' => '1',
+                      'pkg_svc4' => '0',
+                      'pkg_svc5' => '0',
+                    },
+      'location' => 'browse/part_pkg.cgi',
+    },
+    { 'url'      => 'edit/process/part_pkg.cgi',
+      'params'   => {
+                      'pkgpart' => '',
+                      'pkg' => 'Business SLIP/PPP',
+                      'comment' => '$30/setup, $29.99/month',
+                      'setup' => '30',
+                      'recur' => '29.99',
+                      'freq' => '1',
+                      'pkg_svc1' => '0',
+                      'pkg_svc2' => '1',
+                      'pkg_svc3' => '0',
+                      'pkg_svc4' => '1',
+                      'pkg_svc5' => '1',
+                    },
+      'location' => 'browse/part_pkg.cgi',
+    },
+    { 'url'      => 'edit/process/part_pkg.cgi',
+      'params'   => {
+                      'pkgpart' => '',
+                      'pkg' => 'Business SLIP/PPP',
+                      'comment' => '$0/setup, $299.88/year',
+                      'setup' => '0',
+                      'recur' => '299.88',
+                      'freq' => '12',
+                      'pkg_svc1' => '0',
+                      'pkg_svc2' => '1',
+                      'pkg_svc3' => '0',
+                      'pkg_svc4' => '1',
+                      'pkg_svc5' => '1',
+                    },
+      'location' => 'browse/part_pkg.cgi',
+    },
+    { 'url'      => 'edit/process/part_pkg.cgi',
+      'params'   => {
+                      'pkgpart' => '',
+                      'pkg' => 'Business POP mailbox',
+                      'comment' => '$10/setup, $5/month',
+                      'setup' => '10',
+                      'recur' => '5',
+                      'freq' => '1',
+                      'pkg_svc1' => '0',
+                      'pkg_svc2' => '0',
+                      'pkg_svc3' => '1',
+                      'pkg_svc4' => '0',
+                      'pkg_svc5' => '1',
+                    },
+      'location' => 'browse/part_pkg.cgi',
+    },
+    { 'url'      => 'edit/process/part_pkg.cgi',
+      'params'   => {
+                      'pkgpart' => '',
+                      'pkg' => 'UNIX shell',
+                      'comment' => '$20/setup, $9.99/month',
+                      'setup' => '20',
+                      'recur' => '9.99',
+                      'freq' => '1',
+                      'pkg_svc1' => '1',
+                      'pkg_svc2' => '0',
+                      'pkg_svc3' => '0',
+                      'pkg_svc4' => '0',
+                      'pkg_svc5' => '0',
+                    },
+      'location' => 'browse/part_pkg.cgi',
+    },
+    { 'url'      => 'edit/process/part_pkg.cgi',
+      'params'   => {
+                      'pkgpart' => '',
+                      'pkg' => 'Point-to-point T1',
+                      'comment' => '$1000/setup, $1000/month',
+                      'setup' => '1000',
+                      'recur' => '1000',
+                      'freq' => '1',
+                      'pkg_svc1' => '0',
+                      'pkg_svc2' => '0',
+                      'pkg_svc3' => '5',
+                      'pkg_svc4' => '1',
+                      'pkg_svc5' => '5',
+                    },
+      'location' => 'browse/part_pkg.cgi',
+    },
+    { 'url'      => 'edit/process/part_pkg.cgi',
+      'params'   => {
+                      'pkgpart' => '',
+                      'pkg' => 'Cisco 2501 Router',
+                      'comment' => '$2500',
+                      'setup' => '2500',
+                      'recur' => '0',
+                      'freq' => '0',
+                      'pkg_svc1' => '0',
+                      'pkg_svc2' => '0',
+                      'pkg_svc3' => '0',
+                      'pkg_svc4' => '0',
+                      'pkg_svc5' => '0',
+                    },
+      'location' => 'browse/part_pkg.cgi',
+    },
+
+    { 'url'      => 'edit/process/agent_type.cgi',
+      'params'   => {
+                      'typenum' => '',
+                      'atype' => 'Internal Sales',
+                      'pkgpart1' => 'ON',
+                      'pkgpart2' => 'ON',
+                      'pkgpart3' => 'ON',
+                      'pkgpart4' => 'ON',
+                      'pkgpart5' => 'ON',
+                      'pkgpart6' => 'ON',
+                      'pkgpart7' => 'ON',
+                      'pkgpart8' => 'ON',
+                      'pkgpart9' => 'ON',
+                    },
+      'location' => 'browse/agent_type.cgi',
+    },
+
+    { 'url'      => 'edit/process/agent.cgi',
+      'params'   => {
+                      'agentnum' => '',
+                      'agent' => 'Internal Sales',
+                      'typenum' => '1',
+                      'freq' => '',
+                      'prog' => '',
+                    },
+      'location' => 'browse/agent.cgi',
+    },
+
+    { 'url'      => 'edit/process/part_referral.cgi',
+      'params'   => {
+                      'refnum' => '',
+                      'referral' => 'Another customer',
+                    },
+      'location' => 'browse/part_referral.cgi',
+    },
+    { 'url'      => 'edit/process/part_referral.cgi',
+      'params'   => {
+                      'refnum' => '',
+                      'referral' => 'Newspaper ad',
+                    },
+      'location' => 'browse/part_referral.cgi',
+    },
+
+    { 'url'      => 'edit/process/svc_acct_pop.cgi',
+      'params'   => {
+                      'popnum' => '',
+                      'city' => 'Line Lexington',
+                      'state' => 'PA',
+                      'ac' => '215',
+                      'exch' => '996',
+                    },
+      'location' => 'browse/svc_acct_pop.cgi',
+    },
+    { 'url'      => 'edit/process/svc_acct_pop.cgi',
+      'params'   => {
+                      'popnum' => '',
+                      'city' => 'Oakland',
+                      'state' => 'CA',
+                      'ac' => '510',
+                      'exch' => '208',
+                    },
+      'location' => 'browse/svc_acct_pop.cgi',
+    },
+
+    { 'url'      => 'edit/process/cust_main.cgi',
+      'params'   => {
+                      'custnum' => '',
+                      'agentnum' => '1',
+                      'refnum' => '1',
+                      'last' => 'Hogan',
+                      'first' => 'Shawn D.',
+                      'ss' => '',
+                      'company' => 'Digital Point Solutions',
+                      'address1' => '3570 Tony Drive',
+                      'address2' => '',
+                      'city' => 'San Diego',
+                      'state' => 'CA / US',
+                      'zip' => '92122-2307',
+                      'daytime' => '',
+                      'night' => '',
+                      'fax' => '',
+                      'tax' => '',
+                      'invoicing_list_POST' => '',
+                      'invoicing_list' => '',
+                      'payby' => 'BILL',
+                      'CARD_payinfo' => '',
+                      'CARD_month' => '1',
+                      'CARD_year' => '1999',
+                      'CARD_payname' => '',
+                      'BILL_payinfo' => '',
+                      'BILL_month' => '12',
+                      'BILL_year' => '2037',
+                      'BILL_payname' => 'Accounts Payable',
+                      'COMP_payinfo' => '',
+                      'COMP_month' => '1',
+                      'COMP_year' => '1999',
+                      'pkgpart_svcpart' => '1_2',
+                      'username' => 'cyborg',
+                      '_password' => '',
+                      'popnum' => '1',
+                      'otaker' => 'example',
+                    },
+      'location' => 'view/cust_main.cgi?1',
+    },
+    { 'url'      => 'edit/process/cust_main.cgi',
+      'params'   => {
+                      'custnum' => '',
+                      'agentnum' => '1',
+                      'refnum' => '2',
+                      'last' => 'Ford',
+                      'first' => 'Bill',
+                      'ss' => '',
+                      'company' => 'Boardtown Corporation',
+                      'address1' => '116 East Main Street',
+                      'address2' => '',
+                      'city' => 'Starkville',
+                      'state' => 'MS / US',
+                      'zip' => '39759',
+                      'daytime' => '',
+                      'night' => '',
+                      'fax' => '',
+                      'tax' => '',
+                      'invoicing_list_POST' => '',
+                      'invoicing_list' => '',
+                      'payby' => 'BILL',
+                      'CARD_payinfo' => '',
+                      'CARD_month' => '1',
+                      'CARD_year' => '1999',
+                      'CARD_payname' => '',
+                      'BILL_payinfo' => '',
+                      'BILL_month' => '12',
+                      'BILL_year' => '2037',
+                      'BILL_payname' => 'Accounts Payable',
+                      'COMP_payinfo' => '',
+                      'COMP_month' => '1',
+                      'COMP_year' => '1999',
+                      'pkgpart_svcpart' => '3_3',
+                      'username' => 'billf',
+                      '_password' => '',
+                      'popnum' => '',
+                      'otaker' => 'example',
+                    },
+      'location' => 'view/cust_main.cgi?2',
+    },
+
+           
+  );
+}
+